안녕하세요, 로건입니다 👋
여러분, 1편에서 잠깐 흘리고 지나간 이야기를 다시 꺼낼게요. CodePush 시절 우리는 강제 업데이트를 --mandatory 한 줄로 처리했어요. EAS Update에는 그런 1급 옵션이 없습니다. 그래서 우리는 모드라는 개념을 따로 만들기로 했습니다.
이번 편은 그 모드 시스템에 대한 이야기예요. 어떻게 사람이 적는 한 줄이, 시스템을 통과해 클라이언트의 분기까지 도달하는가에 대한 디자인.
모드 이름 네 가지
우리가 정의한 모드는 네 가지예요.
| 모드 | 역할 |
|---|---|
| Stale | 가장 약한 노출 |
| Silent | 사용자가 눈치채지 않는 노출 |
| Soft | 사용자에게 권유하는 노출 |
| Force | 사용자가 받아야만 진행 가능한 노출 |
각 모드가 클라이언트에서 정확히 어떻게 보이는지는 클라이언트 측 구현이 결정해요. 이 글의 관심사는 그게 아니라, 그 결정을 어디에 저장하고, 어떻게 거기까지 전달하는가예요.
데이터 채널을 어디에 둘 것인가
4모드를 정한 다음 문제는, 그 모드를 어디에 적어 넣을 것인가였어요. 옵션이 셋쯤 있었습니다.
- A. EAS update에 커스텀 메타데이터로 첨부 — 깔끔하지만, EAS 측 형식에 의존
- B. 별도 API 엔드포인트로 따로 호출 — 운영 단면이 두 배가 됨
- C. 메시지 문자열의 prefix로 박기 — 가장 천박하지만 가장 단순
우리는 C를 골랐어요. 이유는 단 하나입니다. 메시지는 어차피 사람이 적습니다. 그 한 줄에 모드까지 들어가면, 그 한 번의 입력으로 모든 게 결정돼요. 별도 입력칸이 필요 없고, 누락도 없습니다.
그래서 OTA 메시지의 문법이 이렇게 박혔어요.
<Stale|Silent|Soft|Force>: 메시지 본문
예를 들면 이런 식이에요.
Silent: 로그인 화면 회귀 hotfix
Force: 결제 모듈 보안 패치
Soft: 차트 뷰 신기능 안내
Stale: 카피 오타 정리
이 한 줄이 우리 시스템의 데이터 진실원이에요. 사람이 적은 한 줄이, 코드를 통과하면서 클라이언트의 분기까지 결정합니다.
정규식 강제 — 잘못 입력하면 끝까지 못 간다
문자열 prefix는 천박한 방식이라고 했죠. 천박한 방식은 입력 무결성에서 보통 손해를 봅니다. 그래서 우리는 그 입구에서 한 번 더 못을 박았어요.
// ota-common.js — resolveUpdateArgs() 발췌
function resolveUpdateArgs(rawUpdateChannel, rawInput = "") {
const match = rawInput.match(
/^(?:\s)*(Stale|Silent|Soft|Force)\s*:\s*(.+?)\s*$/,
);
if (!match) {
throw new Error(
`OTA message must use the form "<Stale|Silent|Soft|Force>: 메시지". Input: ${rawInput}`,
);
}
return {
updateChannel: rawUpdateChannel,
updateType: match[1],
updateMessage: match[2],
remoteConfigKey: ["prod", "production"].includes(rawUpdateChannel)
? "appConfig"
: "appConfig_dev",
};
}이 함수가 모든 흐름의 최상단에 있어요. eas-deploy.js도, eas-republish.js도, 가장 먼저 이걸 호출합니다. 잘못 입력하면 EAS 업로드도, Remote Config 갱신도, 알림도 아무것도 시작되지 않습니다.
그게 좋은 점이 두 가지 있어요.
- 반쪽짜리 상태가 만들어지지 않음. "EAS는 올라갔는데 RC는 안 바뀐" 상태를 만들 수가 없어요. 입구에서 막히니까.
- 에러 메시지가 곧 문서. 잘못 입력하면 "이런 형식으로 적어야 해"라고 알려줘요. 신규 운영자가 들어와도 README를 안 읽고도 형식을 알게 됩니다.
같은 함수가 한 줄 더 일을 합니다 — 채널이 production이면 RC의 appConfig 키를, 아니면 appConfig_dev 키를 가리키게 해요. 한 줄짜리지만 환경 분기가 여기서 끝나요.
메시지에서 RC까지 — 한 줄이 흘러가는 길
전체 흐름을 한 번에 그려볼게요.
다이어그램의 마지막 노드 — 클라이언트의 4모드 분기 — 는 이 글의 범위 밖이에요. 그 분기가 어떻게 사용자 경험을 만드는지는 클라이언트 측 구현이 결정합니다. 우리가 여기서 보장하는 것은 단 하나, 그 분기가 읽을 값이 메시지 한 줄에서 정확히 도달한다는 사실이에요.
같은 binary, 다른 모드
마지막으로 짚고 갈 디자인 한 가지. EAS Update의 binary와 우리의 모드는 분리돼 있어요.
- 번들 자체는 같은
eas update. 같은groupId. 같은 JS bundle. - 그 번들에 어떤 모드가 입혀질지는 RC의
otaUpdate한 줄로 결정.
이 분리가 3편에서 다룰 재배포(republish)를 가능하게 해요. 같은 groupId를 staging에 묶을 때와 production에 묶을 때, 서로 다른 모드를 입힐 수 있어요. 같은 binary에 다른 노출 규칙을 줄 수 있다는 뜻이에요.
작은 문법이 강한 운영을 만든다. 한 줄의 입력이 시스템의 끝까지 흘러가게 설계하면, 코드는 짧아지고 운영은 정직해진다.
다음 편에서는 이 모드 시스템 위에 얹은 두 번째 트릭 — staging에서 검증된 바로 그 번들을 production으로 옮기는 republish 흐름을 다뤄볼게요. 재빌드 없는 정직한 승격에 대한 이야기입니다.