토스뱅크가 패키지를 관리하는 방식 #2: 토스뱅크 패키지의 세 가지 기준

토스뱅크가 패키지를 관리하는 방식 #2: 토스뱅크 패키지의 세 가지 기준

토스뱅크 프론트엔드 챕터
2026년 10월 16일

안녕하세요. 토스뱅크에서 패키지 개발 환경을 개선하고 있는 기술그룹입니다.

패키지를 사용하는 쪽(이하 호스트)에서는 패키지 코드가 어떤 브라우저까지 지원하는지, 패키지 개발 중 사용하던 API를 실행 환경에서도 쓸 수 있는지 따로 살펴봐야 했어요. 호스트에서 패키지의 원본 소스 코드를 확인하기도 어려웠어요.

이 문제를 해결하기 위해 패키지가 갖춰야 할 기준을 정했어요. 그리고 각 기준을 충족하려면 무엇이 필요한지 살펴보고, 그에 맞는 빌드 도구와 설정을 선택하기로 했어요.

기준은 세 가지예요.

호스트가 지원하는 실행 환경에서 안정적으로 동작해야 해요

토스뱅크의 각 서비스는 자신의 지원 범위에 맞춰 패키지를 항상 트랜스파일해요. 그럼 최신 문법을 모두 사용해도 호스트 런타임에 문제가 없다고 보장할 수 있을까요?

문법은 호스트의 트랜스파일러가 해당 문법 변환을 지원해야 하고, API는 실행 환경에서 제공하거나 폴리필로 추가해야 해요. 예를 들어 rolldown의 문법 변환 관련 이슈에서는 당시 oxc가 ES2025의 일부 정규식 문법을 구형 환경용으로 변환하지 못한다는 점을 짚었어요. 즉 최종 호스트가 낮은 target을 지정하더라도 도구가 지원하지 않는 변환까지 수행되지는 않는 거예요.

폴리필에도 한계가 있어요. 예를 들어 Proxy는 이를 지원하지 않는 엔진에서 폴리필만으로 전체 동작을 구현할 수 없어요. 폴리필 라이브러리를 추가하는 것만으로 모든 API를 사용할 수 있게 되는 것은 아니에요.

따라서 문법은 호스트의 변환 도구가 처리할 수 있는지, API는 대상 브라우저 또는 호스트의 폴리필이 제공할 수 있는지를 각각 확인해야 해요.

트랜스파일링을 호스트에 맡기더라도 호환성 문제는 패키지 개발 단계에서 미리 확인해야 해요. 그래서 호스트의 API 지원 범위를 타입 검사에 반영했어요.

다만 TypeScript 설정만으로 호환성을 모두 확인할 수는 없으므로, 실제 API 지원 여부와 필요한 문법 변환이 가능한지도 확인해야 해요.

배포 후에도 원본 코드의 의도가 유지되어야 해요

호스트는 자신의 실행 환경에 맞춰 패키지 코드를 변환하고 번들링해요. 저희는 이 작업을 호스트에 맡기고, 패키지 빌드에서는 원본의 문법과 모듈 구조를 최대한 유지하기로 했어요.

문법 변환은 호스트에 맡겨요

패키지를 사용하는 서비스마다 지원하는 브라우저와 런타임이 달라요. 따라서 같은 패키지 코드라도 호스트에 따라 필요한 문법 변환이 달라질 수 있어요.

예를 들어 user?.name을 Optional Chaining을 지원하지 않는 환경에 맞춰 변환하면 다음처럼 바뀔 수 있어요.

const name = user === null || user === void 0 ? void 0 : user.name;

이 상태로 패키지를 배포하면 Optional Chaining을 지원하는 호스트도 변환된 코드를 받게 돼요. 문법에 따라서는 호환성을 위한 함수가 추가되기도 해요. 패키지에서 문법을 미리 낮추면 호스트에 필요하지 않은 코드가 포함되고, 원본과 결과물의 차이도 커져요.

그래서 저희는 패키지의 JavaScript 문법을 유지하고, 실행 환경에 맞춘 문법 변환은 호스트에 맡기기로 했어요. 호스트가 자신의 지원 범위에 맞춰 필요한 변환을 적용하도록 했어요.

모듈 구조도 유지해 전달해요

패키지를 빌드할 때 미리 번들링하면, 원본에서 나눠 둔 파일의 경계가 달라질 수 있어요. 프레임워크가 파일 단위로 코드를 처리하는 경우에는 이 차이가 실제 동작에도 영향을 줄 수 있어요.

예를 들어 Next.js App Router에서는 'use client'를 선언한 파일이 클라이언트 코드의 경계가 돼요. 이 선언이 있는 Counter.tsx와 선언이 없는 formatPrice.ts가 분리돼 있다면, Server Component에서도 formatPrice를 가져와 호출할 수 있어요. 하지만 두 파일을 하나로 번들링하고 전체에 'use client'를 적용하면, formatPrice도 클라이언트 코드로 처리돼 같은 방식으로 호출할 수 없어요.

번들링 과정에서도 이런 경계를 유지할 수 있어요. 다만 패키지 빌드에서 파일별 지시문의 적용 범위까지 고려해야 하고, 이를 위한 설정도 관리해야 해요.

그래서 저희는 원본의 모듈 구조를 유지해 배포하기로 했어요. 호스트가 파일별 지시문과 모듈 경계를 해석할 수 있도록 전달하고, 최종 번들링은 호스트에 맡겼어요.

패키지 빌드에서는 TypeScript를 JavaScript로 변환하고, 실행 환경에 맞춘 문법 변환과 최종 번들링은 호스트가 담당하도록 했어요.

사용처에서 쉽게 디버깅할 수 있어야 해요

패키지가 예상과 다르게 동작하면 사용하는 서비스에서도 원인을 파악할 수 있어야 해요. 함수가 어떤 조건에서 오류를 던지는지, 옵션을 생략하면 어떻게 동작하는지 확인하려면 타입 선언과 문서만으로는 부족할 수 있어요.

이때 가장 빠른 방법은 원본 코드를 직접 확인하는 거예요. 그래서 에디터에서 패키지의 원본 코드를 바로 확인할 수 있도록 declaration sourcemap을 지원하기로 했어요.

declaration sourcemap은 .d.ts의 각 선언을 원본 소스의 해당 위치와 연결해 주는 파일이에요. 이 파일과 원본 파일을 설치된 패키지에 포함하면, 사용하는 쪽에서도 IDE에서 클릭 한 번으로 d.ts 대신 실제 구현을 확인할 수 있어요.

마치며

호스트의 지원 범위에 맞춰 패키지를 개발하고, 문법과 모듈 구조를 유지해 배포하도록 기준을 정했어요. 사용하는 쪽에서는 자신의 환경에 맞춰 문법 변환과 번들링을 진행하고, 문제가 생기면 설치된 패키지의 원본 구현을 바로 확인할 수 있도록 했어요.

저희 기술그룹은 안정성, 예측 가능성, 디버깅 편의성을 기준으로 패키지를 만드는 쪽과 사용하는 쪽의 역할을 정리했어요. 이 기준 덕분에 빌드 도구와 설정을 선택할 때도, 어떤 변환이 필요하고 어떤 파일을 제공해야 하는지 일관되게 판단할 수 있게 됐어요.

뉴스레터가 발행되면
이메일로 알려드릴게요
구독하기