1
이번 작업은 ‘다국어'로 하겠습니다. 근데 이제 ‘귀찮음’ 을 덜어낸..
안녕하세요 Alarmy iOS 개발자 리디입니다.
여러분들의 다국어 작업 생산성은 안녕하신가요?
오늘은 97개국에 30개가 넘는 언어를 배포하는 딜라이트룸의 다국어 작업 프로세스에 대하여 이야기 해보겠습니다.
우선 간략하게 현재의 다국어 파이프라인을 보여드리자면 다음과 같습니다.
어차피 뒤에서 다시 설명 드릴꺼라 완전히 이해하실 필요는 없고, 이런 느낌이다 정도만 받아가셔도 됩니다.
- DesignGroup에서 먼저 필요한 다국어를 Lokalise에 업로드를 해줍니다.
- Lokalise에서는 alarmy-ios-localise repo (submodule) 에 PR을 생성해줍니다.
- LaurineGenerator script를 통해 key들을 static property로 들고 있는Localizations.swift 파일을 만들어줍니다.
- python script로 validCheck를 하고 이를 통과하면 자동으로 머지합니다.
(성공/실패는 slack message로 전송합니다) - submodule에서 alarmy-ios repo로 submodule update 요청을 dispatch합니다.
- alarmy-ios repo 의 develop branch에서는 submodule을 최신으로 업데이트 해줍니다.
이 중 초록색[1,2]은 디자인팀의 작업, 파란색[3,4,5,6] 은 github action을 이용한 자동화 액션들입니다.
즉, 현재 딜라이트룸 iOS 그룹에서 다국어를 위해 하는 노력은 없습니다.
특히 4 번의 작업이 최근에 이뤄지면서 그룹에서 올라오는 PR을 검수하는 비용조차 세이브가 됐습니다. 그리고 오늘은 이 이야기를 해보려고 합니다.
각 전략들에 대해서 설명을 해보면 다음과 같습니다.
(아래 숫자들은 작업 순서로 위의 파이프라인 순서와는 무관합니다.)
1 . Localizations.swift
제가 딜라이트룸에 처음 입사한 2022년 4월에는 딜라이트룸 iOS 그룹도 비효율이 넘치는 방식의 다국어 작업이 이뤄지고 있었습니다.
(하나하나 손으로 복붙을 해야하는…)
그나마 다행인 부분은 복붙 이후 Xcode Build시에 LaurineGenerator 라는 script를 통해 key값을 static property 로 갖는 Localizations.swift 파일을 만들어주는 정도의 자동화는 되어있었습니다. 그리고 이를 팀의 니즈에 맞게 살짝 변형하여 사용하고 있었습니다.
LaurineGenerator는 Base.lproj 의 key를 기반으로 해당 파일을 만들어줬고, 당연하게도 여전히 우리는 꽤 많은 실수를 하고 있었습니다.
ex) key 값을 Base와 다르게 넣는다던가, 혹은 아예 빼먹었다던가..
여기서 발생하는 비효율로 입사 한달 뒤인 5월, 이미 안드로이드 그룹에서 사용주이던 다국어 지원 툴인 Lokalise 를 iOS 도 도입하게 됩니다.
2. Lokalise
Lokalise 가 뭐냐면요?
Lokalise는 소프트웨어, 웹사이트, 모바일 앱, 게임 등 다양한 디지털 제품의 로컬라이제이션(Localization) 과정을 효율적으로 관리할 수 있도록 돕는 클라우드 기반 플랫폼입니다. 로컬라이제이션은 다국어 지원과 관련된 작업으로, 사용자가 전 세계적으로 제품을 사용할 수 있도록 콘텐츠를 번역하고 지역화하는 데 중요한 역할을 합니다.
라고 GPT가 아주 깔끔한 정리를 해주었습니다.
즉, 제품의 다국어 지원을 손쉽게 만들어주는 툴입니다.
간단히 예시를 들어드리면
이렇게 Key와 언어별 value들을 채워주고요. ( Empty 는 자동으로 Base언어로 채워줍니다.)
요렇게 Build 버튼을 누르면 ?!
이렇게 변경사항들이 적용된 PR이 생성됩니다.
어떤가요?
참 쉽죠?
물론 Lokalise 연동 과정에도 많은 사건들이 있었지만,
그건 별로 중요하지 않으니 이정도로 설명을 마치도록 하겠습니다.
3. Submodule
하지만 문제는 여기서도 발생합니다.
당시에 저는 다음과 같이 문제 상황을 잘 정의하고 있었네요.
즉, Lokalise에서 자동으로 PR을 만들어주는 것은 좋은데, 저희가 관리할 수 없는 외부에서의 파일생성은 끊임없는 병합 충돌(conflict) 을 발생하게 만들고, github 전략적으로도 비효율을 야기하고 있었습니다.
그래서 저는 이를 해결하기위한 전략으로 submodule 방식을 제안합니다.
submodule이란?
Git의 Submodule은 하나의 Git 리포지토리 안에 다른 Git 리포지토리를 포함할 수 있도록 하는 기능입니다. 이를 통해 메인 프로젝트에서 독립적인 외부 프로젝트나 라이브러리를 포함하고 관리할 수 있습니다. -by GPT
쉽게 말해서 submodule은 독립적인 repository로 main repository 는에서commit hash로 특정 버젼을 고정해서 사용할 수 있습니다.
저희의 경우에 Localization 을 submodule로 만들 경우 main repository develop branch는 언제나 최신 commit address를 바라보게 하고, 필요에 따라 각 branch 는 (hotfix 혹은 feature branch) 에 따라 변경하는 방식으로 유연한 작업이 가능해 지게 되는거죠.
자 이제 우리는 원하는대로 쉽게 다국어 버젼을 옮겨다닐 수 있게 되었습니다. 언제든 원하는 곳으로 주소를 옮기면 더이상 Rebase 혹은 cherrypick 없이 원하는 버젼의 다국어를 쉽게 적용할 수 있게 되었습니다.
하지만 여기서 끝나면 아쉽겠죠?
4. github action
앞서 1번에서 말씀드린 것 처럼 저희는 LaurineGenerator를 통한 파일관리를 하고 있었습니다. 그리고 다국어 관리가 submodule로 빠졌는데 Xcode에서 이를 관리하는 것은 이상했죠.
작업자가 submodule을 최신화 시키고, Xcode를 빌드하고 이를 다시 머지한다?
상상만 해도 비효율 적이죠.
때문에 이를 해결해줄 좋은 친구인 github action과 친해져보기로 합니다.
github action이란?
GitHub Actions는 GitHub에서 제공하는 CI/CD(Continuous Integration/Continuous Deployment) 도구입니다. 이를 통해 코드를 자동으로 빌드, 테스트, 배포하거나 다양한 워크플로우를 자동화할 수 있습니다.
github내에서 특정 trigger 로 action을 수행하는 친구 정도로 이해하시면 편합니다.
1차 목표는 아주 단순했습니다.
- lokalise 로 시작하는 PR이 open 되면 localization.sh 를 실행한다. 그리고 변경 사항을 commit한다.
on:
pull_request_target:
types:
- opened
jobs:
localization:
if: startsWith(github.head_ref, 'lokalise')
runs-on: macos-latest
steps:
- name: Check out lokalise branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Pull Request
run: |
git pull origin
- name: Run Script
id: script
run: |
ls -al
chmod +x ./Scripts/localization.sh
./Scripts/localization.sh
- name: commit and push
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "[Success] complete localization build"
말끔해보였습니다.
이제 submodule 에 작업자가 머지 후 main repo 에서는 develop은 submodule 최신화만 시켜주면 되니까요.
근데 말끔하지 않죠?
맞습니다.
작업자가 머지도 하고 develop 최신화라니.. 너무 귀찮잖아요?
5. dispatch submodule
그래서 하나의 목표가 추가됩니다.
- PR이 머지되면 main repository 에서는 자동으로 최신화를 시켜줬으면 좋겠어!
저도 github action에 대해서 잘 모르기에 이게 기술적으로 가능한지를 몰랐는데(당연히 가능할 것 같긴 했지만), 찾아보니까 이미 쉽게 되더라구요?
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Workflow
uses: actions/github-script@v6
with:
github-token: ${{ secrets.BUILDER_BOT_1 }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'delightroom',
repo: 'alarmy-ios',
workflow_id: 'update_submodule.yml',
ref: 'develop',
})
요런식으로 submodule에서 던지면
on:
workflow_dispatch:
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.BUILDER_BOT_1 }}
- name: Pull & update submodules recursively
run: |
git submodule update --init --recursive
git submodule update --recursive --remote
- name: Commit & push changes
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'builder-bot-1@delightroom.com'
git add .
git commit -am "Update submodules"
git push
main-repo에서는 이렇게 찰떡같이 받아서 submodule을 최신화 시켜줍니다.
자 이제 귀찮은 submodule 최신화까지!
다국어의 적들을 해치워버렸습니다!
6. 검수.. 검수..
생산성이 좋아진 것은 저희 뿐만이 아니기에 다국어 적용 속도는 점점 빨라지고 Lokalise가 만들어내는 PR 의 갯수는 점차 늘어갔습니다.
마치 무료 배송이 생기자 물량이 늘어나버린 쿠팡 로켓배송 처럼 말이죠.
초기에는 문제는 되지 않았습니다.
어차피 가장 마지막 것만 확인하고 머지하면 되니까요.
이전 PR들은 close시키고 말이죠.
하지만 submodule repo 방문시마다 쌓여있는 PR 은 사람의 마음을 혼란스럽게 만들었고, 개발팀에서는 체크하는 내용이 몇개 없었기에 (사실 Key에만 문제가 없으면 되니까..) 이 부분도 자동화 시켜버리기로 합니다.
더구나 초기 셋팅할 때와는 다르게 든든한 claude와 gpt가 있었기에 이 결정에서의 어려움은 전혀 없었습니다.
그렇게 인공지능 친구들만 믿고 출발한 자동화 열차는 localization이 끝나면 python script(validate_variables.py)를 실행해주는 요런 귀욤둥이 스크립트를 만들어줬습니다.
review-and-merge:
needs: localization
if: ${{ needs.localization.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Checkout base branch
uses: actions/checkout@v3
with:
path: base_branch
ref: ${{ github.base_ref }}
- name: Get First Commit Message
id: commit
uses: actions/github-script@v6
with:
script: |
const { data: commits } = await github.rest.pulls.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
const firstCommit = commits[0];
core.setOutput('message', firstCommit.commit.message);
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install GitPython
run: pip install gitpython
- name: Run validation script
id: validate
run: |
python Scripts/validate_variables.py
env:
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Notify Slack on Validation Failure
if: steps.validate.outcome == 'failure'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
message: "❌ PR Validation Failed\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: `${{ github.head_ref }}`\nCommit: ${{ steps.commit.outputs.message }}\nError: ${{ env.VALIDATION_ERROR }}"
- name: Close PR on Validation Failure
if: steps.validate.outcome == 'failure'
uses: actions/github-script@v6
with:
script: |
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `❌ PR Validation Failed\nError: ${process.env.VALIDATION_ERROR}`
});
- name: Merge PR
if: steps.validate.outputs.can_merge == 'true'
id: merge
uses: actions/github-script@v6
with:
script: |
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.pull_request.number }},
merge_method: 'squash'
});
continue-on-error: true
- name: Notify Slack on Merge Success
if: steps.merge.outcome == 'success'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
message: "🎉 PR Merge Success\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: `${{ github.head_ref }}`\nCommit: ${{ steps.commit.outputs.message }}
여기는 gpt 친구와 뽀짝뽀짝 만들어서 사실 너무 고생을 안해서.. 에피소드가 없네요.
그리고 이왕 하는김에 작업 결과를 slack에서 받아보고 싶어서 요 친구도 추가하게 되었습니다.
(직접 확인하는 거 귀찮잖아요.. 저만 그런거 아니죠..?)
그리고 그 결과는요?!
이렇게 잘 동작하는 것을 확인할 수 있습니다 :)
이제 더 이상 개발자가 다국어에서 해야할 것은 없습니다.
디자이너께서 다국어 PR을 올려주시면
- 알아서 검수 후오류가 있으면 알려주고, 정상일 경우에는 자동 머지 후 submodule 최신화까지 끝낼 테니까요.
혹시나 그간 다국어 작업에 어려움이 있으셨다면, 좋은 아이디어 얻으셨으면 좋겠다는 마음으로 작업 히스토리와 작업 내용에 대하여 글을 정리해봤습니다. 도움이 되셨으면 좋겠고, 적용하시기 귀찮다면 Delightroom에 합류하셔도 좋습니다 ㅎㅎ
끝까지 읽어주셔서 감사합니다.
⏰ 딜라이트룸에서 알라미와 함께 아침을 바꿀 분들을 모십니다 🙌
- 딜라이트룸 채용 공고 살펴보기 ⏰
(+ 여기서 지원하면 입사 축하금 100만원💸)