RN 멀티 앱 아키텍처 02. 앱 ID를 여러 파일에 복사하지 않기로 했다 글 표지
React Native아키텍처설정 관리Zod

RN 멀티 앱 아키텍처 02. 앱 ID를 여러 파일에 복사하지 않기로 했다

두 RN 앱의 개발·운영 설정을 한 원본에서 관리하고, TypeScript·Gradle·xcconfig 생성물과 실제 참조까지 검사하도록 구성한 기록입니다.

연재 완료

RN 멀티 앱 아키텍처

읽는 중02

웹 모노레포 경험을 바탕으로 두 RN 앱을 구성하며 공통 코드, 네이티브 설정, 웹·모바일 UI의 경계를 정한 기록입니다.

전체 목차
  1. 01RN 앱 두 개를 한 저장소에 두고, 배포는 따로 가져가기
  2. 02앱 ID를 여러 파일에 복사하지 않기로 했다현재 글
  3. 03웹과 RN에서 같은 버튼을 쓰려면 어디까지 같아야 할까

삼성화재 컨시어지

임직원·배달원 앱의 식별자와 플랫폼별 설정을 관리하도록 구성한 과정을 정리했습니다.

프로젝트와 담당 역할 보기 →

직원용 앱과 배달원용 앱을 하나의 RN 저장소에 두면서, 네이티브 프로젝트는 각각 만들었다. 각 앱이 자기 버전과 배포 대상을 갖게 하려는 선택이었다.

그러고 나니 관리할 설정이 꽤 많았다. 앱이 두 개이고, 각각 개발용과 운영용 환경이 있다. Android와 iOS에 값을 넣어야 하고, JS에서도 어떤 앱과 환경으로 실행됐는지 알아야 한다.

같은 앱을 가리키는 값인데 사용하는 곳마다 따로 적혀 있으면 수정할 때마다 짝을 맞춰야 한다. 이 부분은 사람의 기억에 맡기고 싶지 않았다. 앱 식별자와 관련 설정의 원본을 한곳에 두고, 필요한 파일을 생성하도록 만들었다.

앱은 둘인데 설정을 읽는 곳은 더 많았다

Android는 application ID와 namespace를 사용하고, iOS는 bundle identifier와 scheme을 사용한다. 앱을 여는 URL scheme도 필요하다. 배포 스크립트는 어느 앱의 어떤 환경을 처리할지 알아야 한다.

여기서 모든 식별자가 같은 문자열이어야 하는 건 아니다. Android의 namespace와 설치 대상을 구분하는 application ID도 역할이 다르다. 각 값이 무엇을 뜻하는지 구분하면서 같은 앱의 설정으로 관리할 수 있어야 했다.

수식 사이에서 복잡한 계산을 하는 표정의 여성
앱은 두 개라고 했지, 설정도 두 개라고는 안 했다.

원본 파일은 config/applications.yml로 정했다. 실제 파일에는 프로젝트 이름과 배포에 필요한 값도 있지만, 구조를 설명하기 위해 식별자 부분만 줄이면 이런 모양이다. 아래 식별자는 예시 값이다.

applications:
  employee:
    androidNamespace: com.example.employee
    environments:
      dev:
        applicationId: com.example.employee.dev
        urlScheme: example-employee-dev
      production:
        applicationId: com.example.employee
        urlScheme: example-employee
  courier:
    androidNamespace: com.example.courier
    environments:
      dev:
        applicationId: com.example.courier.dev
        urlScheme: example-courier-dev
      production:
        applicationId: com.example.courier
        urlScheme: example-courier

실제 설정을 바꿀 때는 이 파일에서 시작한다. TypeScript와 네이티브 설정 파일을 각각 열어 같은 값을 입력하는 순서를 없애고 싶었다.

YAML을 읽었다고 올바른 설정은 아니다

YAML 문법에 맞게 썼다는 것과 이 프로젝트에서 사용할 수 있는 설정이라는 건 다르다. 두 앱에 같은 application ID를 적어도 YAML 파서는 파일을 읽을 수 있다.

그래서 파싱한 결과를 바로 앱 설정 타입으로 취급하지 않았다. unknown으로 받은 뒤 Zod 스키마를 통과한 값만 사용한다. 파일을 읽는 코드에서 핵심은 두 줄이다.

const value: unknown = parse(source);
return applicationsManifestSchema.parse(value);

스키마에는 필드의 형식뿐 아니라 설정 사이의 관계도 넣었다. 직원용·배달원용 앱의 개발·운영 환경을 모두 모아 application ID와 URL scheme이 중복되지 않는지 확인한다. 앱 키와 audience가 맞는지, 버전 파일이 해당 앱의 디렉터리에 속하는지도 검사한다.

문자열이라는 사실만 확인하면 이런 관계는 놓치기 쉽다. 이 설정에서는 개별 값의 모양과 함께 다른 설정에 어떤 값을 적었는지도 확인해야 했다. Zod의 입력 검증과 타입 추론 방식은 공식 사용 안내에 정리돼 있다.

원본 하나에서 다섯 파일 만들기

검증을 통과한 설정으로 필요한 파일을 생성한다. 공통 TypeScript 파일 하나와 두 앱의 Android·iOS 파일을 합쳐 다섯 개다.

config/applications.yml
        ↓ 읽기·검증
scripts/applications.ts
        ├─ packages/app-runtime/src/config/generatedApplications.ts
        ├─ apps/employee/android/app/application-identities.gradle
        ├─ apps/employee/ios/AppIdentity.xcconfig
        ├─ apps/courier/android/app/application-identities.gradle
        └─ apps/courier/ios/AppIdentity.xcconfig

TypeScript는 런타임에서 읽을 객체를, Android는 Gradle에서 참조할 설정을, iOS는 빌드 설정에 넣을 변수를 받는다. 같은 원본을 각 도구가 읽을 수 있는 형태로 바꾸는 것이다.

생성 명령은 npm run applications:write로 만들었다. 생성 파일의 첫 줄에는 이 명령으로 만들어진 파일이라는 표시도 넣었다. 값을 바꾸고 싶을 때 어느 파일로 돌아가야 하는지 알 수 있도록 했다.

파일 수 자체는 줄지 않았다. 네이티브 도구가 읽어야 하는 파일은 여전히 필요했다. 대신 같은 의미의 값을 직접 관리하는 위치를 줄였다.

생성한 파일이 오래된 상태라면

원본을 수정한 뒤 생성 명령을 깜빡할 수도 있다. 생성 파일만 직접 수정하는 경우도 생각해야 했다. 원본과 결과물이 어긋난 채 저장소에 들어오면 한곳에서 관리하기로 한 의미가 약해진다.

CI에는 npm run applications:check를 넣었다. 현재 원본으로 나와야 할 내용을 다시 계산한 뒤, 저장소의 생성 파일과 비교한다. 관련 코드를 줄이면 다음과 같다.

const outputs = renderGeneratedApplications(
  readApplicationsManifest(repositoryRoot),
  repositoryRoot
);
 
for (const [path, expected] of outputs) {
  const actual = readRequiredFile(path, 'Generated file is missing');
  if (actual !== expected) {
    throw new Error(`Generated application file drifted: ${path}`);
  }
}

검사 중에는 파일을 덮어쓰지 않는다. 차이가 있으면 실패하게 해서, 원본과 생성물을 같은 변경에 포함하도록 했다. 생성 결과가 바뀌는 이유를 코드 리뷰에서도 볼 수 있다.

통과할 수 없다고 외치며 길을 막는 반지의 제왕의 간달프
원본과 다른 생성물은 지나갈 수 없습니다.

중복 application ID를 거부하는 경우와 필요한 앱별 파일을 생성하는 경우는 테스트 코드에도 남겨두었다.

파일이 맞아도 앱이 그 파일을 읽어야 한다

생성 파일끼리 일치하는지만 보면 아직 확인하지 못한 부분이 있다. Gradle이나 Xcode 프로젝트가 다른 곳에 적힌 값을 계속 사용하고 있다면, 올바른 파일을 생성해도 앱에 적용되지 않는다.

그래서 검사는 생성물에서 끝내지 않았다. Android의 build.gradle이 생성한 설정 파일을 포함하는지, iOS 설정이 AppIdentity.xcconfig를 포함하는지 확인한다. Xcode 프로젝트에 필요한 변수 참조가 있는지, 선택한 scheme 파일이 존재하는지도 살핀다.

생성 설정 밖에 같은 식별자를 다시 적어두는 것도 검사 대상이다. Kotlin 진입 파일의 경로와 package 선언, RN CLI가 선택하는 앱도 원본 설정과 맞는지 확인하도록 했다.

이 검사가 실제 설치와 실행을 대신하지는 않는다. 현재는 파일 내용과 참조 규칙을 확인하는 방식이라 네이티브 프로젝트의 형식이 달라지면 검사 코드도 같이 관리해야 한다. 원본에 적은 값 자체가 스토어에서 원하는 값인지 판단하는 일도 별도로 남는다.

생성 스크립트도 관리할 코드다

설정 생성은 공짜가 아니었다. YAML 스키마와 생성 코드가 생겼고, 네이티브 파일 구조를 바꿀 때 확인할 곳도 늘었다. 앱이 하나이고 설정을 거의 바꾸지 않는 프로젝트라면 이 단계를 추가하는 게 오히려 번거로울 수 있다.

이번에는 두 앱의 개발·운영 환경을 함께 관리해야 했다. 같은 설정을 여러 형식으로 표현해야 한다는 요구가 이미 있었기 때문에, 원본을 정하고 생성물의 일치 여부를 검사하는 방식이 맞았다.

앱 설정을 수정할 때의 순서도 정해졌다. 원본을 바꾸고, 생성하고, 변경 내용을 확인한다. 어디에 같은 값을 또 적어뒀는지 찾아다니는 일부터 시작하지 않아도 된다.

3편에서는 공통 UI를 살펴보려 한다. 웹과 RN의 버튼은 비슷하게 생겼지만, 어느 부분을 맞추고 어느 부분은 따로 구현할지가 또 다른 문제였다.