자바스크립트 모듈 설계와 순환 의존성 해결법

자바스크립트 모듈 설계와 순환 의존성 해결법 관련 이미지
Photo by Elchinator via Pixabay

자바스크립트 프로젝트가 커지면 하나의 파일에 모든 기능을 담을 수 없어 코드를 모듈로 나눕니다. ES 모듈의 import와 export는 파일 사이의 공개 계약을 표현하고 번들러가 사용하지 않는 코드를 제거하는 기반이 됩니다. 하지만 파일을 많이 쪼개는 것만으로 좋은 구조가 되지는 않습니다. 서로가 서로를 가져오는 순환 의존성과 import 시점의 부수 효과가 늘면 초기화 순서에 따라 값이 undefined가 되거나 테스트가 어려워질 수 있습니다. 핵심은 기능의 책임과 의존 방향을 명확하게 하고 공개 API를 작게 유지하는 것입니다.

named export와 default export 선택하기

named export는 가져올 이름이 정해져 있어 검색과 자동 리팩터링이 쉽고 한 모듈의 여러 기능을 명확히 표현할 수 있습니다. default export는 모듈당 하나의 대표 기능을 나타낼 때 편하지만 가져오는 쪽에서 임의의 이름을 붙일 수 있어 프로젝트 전체의 용어가 달라질 수 있습니다. 팀에서 기준을 정하고, 유틸리티 모듈은 named export를 우선하는 방식이 실무에서 관리하기 쉽습니다.

모듈 경계는 기술보다 기능을 기준으로

모든 함수, 타입, API 호출을 각각 전역 폴더에 모으면 하나의 기능을 수정할 때 여러 디렉터리를 오가야 합니다. 주문, 사용자, 결제처럼 업무 기능을 중심으로 관련 코드와 테스트를 가까이 두고 외부에 필요한 항목만 index 파일의 공개 API로 노출할 수 있습니다. 내부 파일을 다른 기능이 깊은 경로로 직접 import하면 구조 변경이 어려우므로 패키지 경계를 지킵니다.

좋은 공개 API 체크포인트

  • 외부 사용자가 알아야 할 함수와 타입만 export합니다.
  • 내부 저장 구조와 라이브러리 객체를 그대로 노출하지 않습니다.
  • 함수 이름은 구현보다 업무 의도를 표현합니다.
  • 입력과 반환값, 발생 가능한 오류를 문서화합니다.
  • 호환성을 깨는 변경은 버전과 마이그레이션 방법을 제공합니다.

순환 의존성이 문제를 만드는 이유

A 모듈이 B를 import하고 B가 다시 A를 import하면 모듈 로딩 과정에서 아직 초기화되지 않은 값을 읽을 수 있습니다. ES 모듈은 값의 살아 있는 바인딩을 제공하지만 선언과 접근 시점에 따라 오류가 생길 수 있으며, CommonJS와 섞이면 동작이 더 복잡해집니다. 테스트에서 우연히 통과해도 번들 순서나 실행 환경이 바뀌면 실패할 수 있습니다.

구조 원칙: 순환 의존성은 import 순서를 조작해 숨기기보다 공통 계약을 더 낮은 계층으로 분리하거나 이벤트와 의존성 주입으로 방향을 끊어야 합니다.

순환을 끊는 실전 방법

두 모듈이 함께 사용하는 타입과 작은 순수 함수를 별도 공통 모듈로 옮길 수 있습니다. 상위 서비스가 두 기능을 조합하도록 하고 하위 모듈끼리 직접 호출하지 않는 방법도 있습니다. 한 기능이 다른 기능의 구체 구현을 호출해야 한다면 필요한 인터페이스나 콜백을 인자로 주입해 의존 방향을 역전합니다. 단순히 모든 코드를 거대한 common 파일로 옮기면 새로운 결합 덩어리가 생기므로 업무 의미가 있는 경계를 유지합니다.

import 시점의 부수 효과 줄이기

모듈을 가져오는 순간 데이터베이스 연결, 이벤트 리스너 등록, 환경 변수 검증이 실행되면 import 순서가 애플리케이션 상태를 바꿉니다. 테스트가 모듈을 불러오기만 해도 외부 시스템에 접근할 수 있습니다. 초기화는 명시적인 함수로 제공하고 애플리케이션 시작점에서 호출하는 편이 안전합니다. 꼭 한 번 실행해야 하는 설정도 중복 호출과 종료 시 정리 방법을 설계합니다.

동적 import와 코드 분할

import()는 Promise를 반환하며 필요한 시점에 모듈을 불러올 수 있습니다. 무거운 편집기와 관리자 기능을 라우트별로 분리하면 초기 번들을 줄일 수 있습니다. 그러나 사용자 클릭 뒤 처음 다운로드하면 대기 시간이 생기므로 로딩 화면과 실패 재시도를 제공해야 합니다. 사용 가능성이 높은 기능은 브라우저가 한가할 때 미리 가져올 수 있지만 무제한 프리로드는 데이터 사용량을 늘립니다.

트리 셰이킹이 동작하는 조건

번들러는 정적인 import와 export를 분석해 사용하지 않는 코드를 제거할 수 있습니다. 동적으로 경로를 조합하거나 모듈 최상위에 부수 효과가 많으면 제거가 어려워집니다. 패키지의 sideEffects 설정을 잘못 지정하면 필요한 CSS나 초기화 코드까지 사라질 수 있습니다. 번들 분석 결과로 실제 포함된 모듈을 확인하고, 라이브러리 전체를 namespace로 가져오기보다 필요한 기능만 import합니다.

브라우저와 Node.js 연동 시 주의점

브라우저와 Node.js는 ES 모듈을 지원하지만 파일 확장자, package.json의 type, 경로 해석 규칙이 다를 수 있습니다. CommonJS의 require와 ES import를 섞을 때 default 내보내기 형태가 예상과 달라질 수 있습니다. 빌드 도구만 통과하는 별칭 경로는 테스트 러너와 서버도 같은 설정을 알아야 합니다. 실행 환경별 설정을 한곳에서 관리하고 배포 산출물을 실제 런타임에서 시험합니다.

검증과 자동화

  1. 모듈의 공개 import 경로를 테스트로 고정합니다.
  2. 순환 의존성 탐지 도구를 CI에서 실행합니다.
  3. 내부 깊은 경로 import를 린트 규칙으로 제한합니다.
  4. 모듈을 import할 때 외부 부수 효과가 없는지 확인합니다.
  5. 동적 import의 로딩과 실패 화면을 테스트합니다.
  6. 번들 분석으로 중복 라이브러리와 청크 크기를 확인합니다.
  7. 브라우저와 Node 대상 빌드를 각각 실행 검증합니다.

barrel 파일이라 불리는 index 모듈은 공개 API를 단순하게 만들지만 모든 하위 모듈을 다시 export하면 순환 관계와 불필요한 로딩을 만들 수 있습니다. 기능 경계의 입구에만 제한적으로 사용하고 내부 코드에서는 가능한 직접적이고 일관된 경로를 선택합니다.

모듈 설계의 목적은 파일 수를 늘리는 것이 아니라 변경의 영향을 제한하는 것입니다. 상위 기능이 하위 기능에 의존하는 한 방향을 유지하고 초기화와 외부 접근을 명시적으로 만들면 코드 분할, 테스트, 재사용이 쉬워집니다. 순환 관계가 보일 때는 도구로 숨기기보다 책임의 경계가 잘못됐다는 신호로 활용하는 것이 좋습니다.

한 줄 요약: 자바스크립트 모듈은 작은 공개 API와 단방향 의존성, 명시적 초기화를 지킬 때 순환 오류와 번들 낭비를 줄일 수 있습니다.