토스뱅크 프론트엔드 챕터는 10월 15일 사내에서 공유하는 패키지를 ESM만 제공하는 방식으로 전환했다고 밝혔다. 이전에는 ESM과 CommonJS로 각각 빌드하고 package.json의 exports에서 호출 방식별 코드와 타입을 연결했다. 팀은 두 출력을 만드는 일 외에도 실행 경로와 외부 의존성의 타입 연결을 함께 확인해야 하는 비용이 있었다고 설명했다.
호환 형식 두 개를 유지하는 비용
전환의 전제는 사용 환경을 파악하고 조정할 수 있는 사내 패키지라는 점이다. 구버전 실행 환경까지 폭넓게 지원해야 하는 공개 라이브러리와 조건이 다르다. 이 사례의 핵심은 모든 패키지에 단일 형식을 권하는 데 있지 않고, 실제 사용처가 요구하는 호환 범위를 다시 정한 데 있다.
한 버전만 설치해도 컨텍스트가 갈릴 수 있다
원문은 리액트 컨텍스트를 예로 든다. 서비스는 ESM 파일을, 서비스의 의존 패키지는 CommonJS 파일을 읽고 두 파일이 각각 컨텍스트를 만들면 객체가 두 개가 된다. Provider가 값을 넣은 컨텍스트와 Hook이 읽는 컨텍스트가 달라져 감싼 값이 보이지 않을 수 있다. 패키지를 여러 버전 설치했을 때만 생기는 문제가 아니다.
노드의 공식 문서도 두 구현이 함께 로드될 때 생성자와 객체의 정체성이 달라지는 이중 패키지 위험을 설명한다. 동일한 이름으로 불러와도 같은 인스턴스가 보장되지 않는 것이다. 모듈 내부 캐시나 공유 상태를 가진 코드라면 파일 형식 선택은 단순한 배포 용량 문제가 아니라 실행 의미에 영향을 준다.
CommonJS 사용처가 사라지지 않아도 가능한 선택
ESM 단일 배포가 기존 CommonJS 코드를 전부 ESM 문법으로 고쳐야 한다는 뜻은 아니다. 노드 공식 변경 기록은 22.12.0부터 require로 ESM을 읽는 기능이 별도 실험 플래그 뒤에 있지 않다고 설명한다. 토스뱅크도 이 변화를 전환 판단의 근거로 제시했다. 다만 불러오는 ESM과 그 의존 그래프는 동기적으로 처리할 수 있어야 하며 최상위 await가 없어야 한다.
로딩 결과에도 주의가 필요하다. 일반적으로 require가 돌려주는 것은 ESM의 모듈 네임스페이스 객체다. 기존 CommonJS 출력의 반환 형태와 동일하다고 가정하면 기본 내보내기나 이름 있는 내보내기의 사용 방식이 달라질 수 있다. 버전 조건과 함께 실제 호출부를 확인해야 하는 이유다.
같은 이름을 읽었다는 사실만으로 충분하지 않다
실행 코드를 찾는지, 같은 상태 객체를 공유하는지, 타입 선언까지 연결되는지는 각각 다른 검사다. 한 버전만 설치했다는 사실은 두 출력에서 별도 컨텍스트를 만드는 문제를 배제하지 못하고, 실행에 성공해도 타입이 올바르게 해석됐다는 증거가 되지는 않는다.
따라서 소비 서비스의 import와 require 경로를 모두 보는 편이 전환 목적에 맞는다. 런타임 버전과 동기 로딩 조건, 실제 내보내기 형태, 타입 검사 결과를 함께 확인하면 형식을 하나 줄인 뒤 어떤 호환 문제가 남는지 구체적으로 파악할 수 있다. 이 글은 해당 패키지를 설치해 시험한 결과는 아니다.
| 층위 | 질문 | 통과만으로 부족한 판단 |
|---|---|---|
| 로딩 | 실행 파일을 찾고 읽는가 | 같은 컨텍스트 공유 보장 아님 |
| 객체 정체성 | Provider와 Hook이 같은 객체를 보나 | 타입 선언 연결 보장 아님 |
| 타입 | 선언 경로와 실제 API가 맞나 | 지원 런타임 실행 보장 아님 |
공용 패키지의 ESM 단일화는 같은 코드가 다른 경로로 불려 상태와 타입이 갈리는 문제를 줄이는 선택이다. 형식의 통일만으로 전환이 끝나지는 않으며 실행 버전과 최상위 await 등 호환 조건을 함께 맞춰야 한다.
ARCH.B 의견
타입스크립트의 공식 예시는 require 조건이 선택되면 대응하는 .cts나 .d.cts 등을 찾고, 타입을 못 찾은 뒤 자바스크립트 파일만 해석되면 가져온 값이 any가 될 수 있다고 설명한다. noImplicitAny를 켰다면 이 상황에 오류가 발생한다. 따라서 원문의 타입 유실 사례를 모든 설정에서 조용히 통과하는 동작으로 확대해서는 안 된다.
외부 라이브러리의 타입을 섞어 공개한 UI 속성이 any에 영향을 받으면 잘못된 속성을 잡는 능력이 약해질 수 있다. 사용처가 실행 코드를 찾는지와 선언 파일까지 올바르게 연결되는지를 나누어 점검해야 한다. ESM 출력 하나로 줄여도 의존성 선언과 소비자의 모듈 해석 설정을 검증하는 일은 남는다.
노드 문서는 이중 형식을 유지하면서 위험을 줄이는 방법으로 CommonJS 구현을 감싸는 ESM 래퍼와 상태를 공유 파일로 분리하는 구성을 소개한다. 단일 배포만이 유일한 해결책은 아니다. 오래된 런타임을 지원해야 한다면 이런 설계와 추가 검증 비용을 감수하는 선택이 여전히 가능하다. 반대로 소비 환경을 함께 올릴 수 있다면 구현 수를 줄이는 편이 관리 범위를 줄일 수 있다. 결정 기준은 유행하는 모듈 형식보다 지원해야 할 실행 환경, 상태 공유 방식, 도구 체인의 호환성에 있다. 토스뱅크의 판단은 그중 사용처를 통제할 수 있다는 조건을 활용한 사례로 읽는 편이 정확하다.
토스뱅크는 도구 버전과 설정을 맞추고 패키지 호출 및 타입 검사를 확인한 뒤 전환했다고 밝혔다. 만드는 쪽은 두 구현을 함께 관리하는 부담을, 사용하는 쪽은 어느 구현이 로드됐는지 추적하는 부담을 덜었다는 설명이다. 원문은 빌드 시간이나 번들 크기의 전후 수치를 제시하지 않았으므로 정량 성능 개선으로 받아들일 근거는 없다. 다른 팀이 적용한다면 배포 형식을 바꾸기 전에 대표 서비스의 실제 로딩과 타입 해석을 확인하는 절차가 이 사례에서 얻을 수 있는 실질적인 출발점이다.

