React Native에서 패키지 내보내기 지원
React Native 0.72 버전이 출시되면서, 자바스크립트 빌드 도구인 Metro는 이제 package.json
의 "exports"
필드에 대한 베타 지원을 포함한다. 이 기능을 활성화하면 다음과 같은 기능이 추가된다:
- React Native 프로젝트가 더 많은 npm 패키지를 바로 사용할 수 있게 된다
- 패키지가 자체 API를 정의하고 React Native를 타겟팅할 수 있는 새로운 기능이 추가된다
- 패키지 해결 방식에 일부 변경 사항이 발생한다 (특정 경우에 한해)
이 글에서는 패키지 익스포트가 어떻게 작동하는지, 그리고 이러한 변경 사항이 React Native 앱 개발자나 패키지 관리자에게 어떤 의미를 가지는지 설명한다.
패키지 내보내기(Exports)란?
Node.js 12.7.0에서 도입된 패키지 내보내기는 npm 패키지가 **진입점(entry points)**을 지정하는 현대적인 방법이다. 이는 외부에서 임포트할 수 있는 패키지 하위 경로와 해당 파일들이 어떤 파일로 해석되어야 하는지를 매핑한다.
"exports"
를 지원하면 React Native 프로젝트가 더 넓은 JavaScript 생태계와 잘 동작하도록 개선된다(현재 약 16.6k 패키지에서 사용 중). 또한 패키지 작성자에게 React Native를 대상으로 하는 멀티플랫폼 패키지를 위한 표준화된 기능 세트를 제공한다.
"exports"
는 package.json
파일에서 "main"
과 함께 사용하거나 대체할 수 있다.
{
"name": "@storybook/addon-actions",
"main": "./dist/index.js",
...
"exports": {
".": {
"node": "./dist/index.js",
"import": "./dist/index.mjs",
"default": "./dist/index.js"
},
"./preview": {
"import": "./dist/preview.mjs",
"default": "./dist/preview.js"
},
...
"./package.json": "./package.json"
}
}
다음은 @storybook/addon-actions
패키지의 다양한 하위 경로를 임포트하여 사용하는 앱 코드 예제다.
import {action} from '@storybook/addon-actions';
// -> '@storybook/addon-actions/dist/index.js'
import {action} from '@storybook/addon-actions/preview';
// -> '@storybook/addon-actions/dist/preview.js'
import helpers from '@storybook/addon-actions/src/preset/addArgsHelpers';
// 접근 불가 - "exports"에 정의되지 않음!
패키지 내보내기의 주요 기능은 다음과 같다:
- 패키지 캡슐화:
"exports"
에 정의된 하위 경로만 외부에서 임포트할 수 있다. 이를 통해 패키지는 공개 API를 제어할 수 있다. - 하위 경로 별칭: 패키지는 파일 위치를 다른 곳으로 매핑하는 커스텀 하위 경로를 정의할 수 있다(하위 경로 패턴 포함). 이를 통해 파일 위치를 변경하더라도 공개 API를 유지할 수 있다.
- 조건부 내보내기: 환경에 따라 하위 경로가 다른 파일로 해석될 수 있다. 예를 들어,
"node"
,"browser"
,"react-native"
런타임을 대상으로 다른 파일을 지정할 수 있다. 이는"browser"
필드 사양을 대체한다.
"exports"
의 전체 기능은 Node.js 패키지 진입점 사양에 자세히 설명되어 있다.
이 기능은 기존 React Native 개념(예: 플랫폼별 확장자)과 겹치며, "exports"
가 npm 생태계에서 이미 사용되고 있기 때문에, React Native 커뮤니티와 협력하여 개발자 요구에 맞는 구현을 보장했다(PR, 최종 RFC).
앱 개발자를 위한 가이드
Package Exports 기능을 현재 베타 버전에서 활성화할 수 있다.
- Package Exports 기능을 사용하는 패키지(예: Firebase 및 Storybook)에 대한 임포트가 이제 의도한 대로 작동한다.
- Metro를 사용하는 React Native for Web 프로젝트에서 이제
"browser"
조건부 익스포트를 사용할 수 있어, 기존의 우회 방법이 필요 없어졌다.
Package Exports를 활성화하면 특정 프로젝트에 영향을 미칠 수 있는 일부 엣지 케이스의 주요 변경 사항이 발생한다. 이러한 변경 사항을 지금 바로 테스트할 수 있다.
향후 React Native 릴리스에서는 Package Exports가 기본적으로 활성화될 예정이다. 닭과 달걀의 상황처럼, 이전에는 React Native 앱이 일부 패키지가 "exports"
로 마이그레이션하는 것을 방해하거나, "react-native"
루트 필드 탈출구를 사용해야 했다. Metro에서 이러한 기능을 지원하면 생태계가 더 나아갈 수 있다.
패키지 내보내기 활성화 (베타)
여러분의 앱에서 패키지 내보내기 기능을 활성화하려면 metro.config.js 파일에서 resolver.unstable_enablePackageExports
옵션을 사용하면 된다.
const config = {
// ...
resolver: {
unstable_enablePackageExports: true,
},
};
Metro는 조건부 내보내기 동작을 구성하는 두 가지 추가 리졸버 옵션을 제공한다:
unstable_conditionNames
— 조건부 내보내기를 해결할 때 사용할 조건 이름 집합. 기본적으로['require', 'import', 'react-native']
와 일치한다.unstable_conditionsByPlatform
— 특정 플랫폼 타겟에 대해 해결할 때 추가로 사용할 조건 이름. 기본적으로 플랫폼이'web'
일 때'browser'
와 일치한다.
React Native Jest 프리셋을 사용하는 것을 잊지 말자! Jest는 기본적으로 패키지 내보내기를 지원한다. 테스트에서는 testEnvironmentOptions
옵션을 사용해 customExportConditions
를 재정의할 수 있다.
TypeScript를 사용하는 경우, 프로젝트의 tsconfig.json
에서 moduleResolution: 'bundler'
와 resolvePackageJsonImports: false
를 설정해 해결 동작을 일치시킬 수 있다.
프로젝트 변경 사항 검증하기
기존 프로젝트에서는 unstable_enablePackageExports
를 활성화한 후 해결된 변경 사항이 있는지 확인하기 위해 다음 단계를 따르기를 권장한다. 이는 일회성 과정이다. 아무런 변경 사항이 없을 가능성이 높지만, 개발자들이 확실히 선택할 수 있도록 하기 위함이다.
💡 프로젝트 변경 사항 검증하기
Yarn을 사용하지 않는다면, yarn
을 npx
(또는 프로젝트에서 사용하는 관련 도구)로 대체한다.
-
모든 해결된 의존성 가져오기 (변경 전):
# index.js를 App.js와 같은 엔트리 파일로 대체할 수 있다
yarn metro get-dependencies index.js --platform android --output before.txt- Expo CLI:
metro.config.js
파일이 아직 없는 경우npx expo customize metro.config.js
를 실행한다. - 전체 범위를 위해
--platform android
를 앱에서 사용하는 다른 플랫폼(예:ios
,web
)으로 대체한다.
- Expo CLI:
-
metro.config.js
에서resolver.unstable_enablePackageExports
를 활성화한다. -
모든 해결된 의존성 가져오기 (변경 후):
yarn metro get-dependencies index.js --platform android --output after.txt
-
비교하기:
diff before.txt after.txt
주요 변경 사항
Metro에서 패키지 내보내기(Package Exports) 기능을 스펙에 맞게 구현하기로 결정했다. 이로 인해 일부 주요 변경 사항이 발생하지만, 기존의 import 방식을 사용하는 앱이 점진적으로 마이그레이션할 수 있도록 하위 호환성은 유지한다.
가장 중요한 변경 사항은 패키지가 "exports"
를 제공할 경우, 해당 필드를 다른 package.json
필드보다 우선적으로 참조한다는 점이다. 그리고 일치하는 하위 경로 타겟을 직접 사용한다.
- Metro는 import 지정자에 대해
sourceExts
를 확장하지 않는다. - Metro는 타겟 파일에 대해 플랫폼별 확장자를 해석하지 않는다.
더 자세한 내용은 Metro 문서의 주요 변경 사항 섹션을 참고한다.
패키지 캡슐화가 느슨하게 적용된다
Metro가 "exports"
에 나열되지 않은 하위 경로를 만나면, 기존 해결 방식으로 되돌아간다. 이는 기존 React Native 프로젝트에서 허용되었던 import와의 호환성을 유지하기 위한 기능으로, 사용자의 불편을 줄이기 위한 목적이다.
에러를 발생시키는 대신, Metro는 경고를 기록한다.
warn: "foo/private/fn.js" 모듈을 import했지만, 이 모듈은 "foo"의 "exports"에 나열되어 있지 않습니다. 호출 위치를 업데이트하거나 패키지 관리자에게 이 API를 노출하도록 요청하는 것을 고려해 보세요.
앞으로 Node의 기본 동작과 일치시키기 위해 패키지 캡슐화에 대한 엄격 모드를 구현할 계획이다. 따라서 모든 개발자는 사용자에게 이러한 경고가 발생하면 이를 해결하는 것을 권장한다.
패키지 관리자를 위한 안내 (미리보기)
출시 계획에 따르면, 올해 말 출시 예정인 React Native 0.73 버전부터 대부분의 프로젝트에서 Package Exports 기능이 활성화된다.
현재로서는 "main"
필드와 기존 패키지 해결 기능에 대한 지원을 단계적으로 제거할 계획이 없다.
Package Exports는 패키지 내부에 대한 접근을 제한하고, 라이브러리가 React Native와 React Native for Web을 대상으로 더 예측 가능한 기능을 제공할 수 있게 한다.
"exports"
를 현재 사용 중이라면
여러분의 패키지가 현재 "react-native"
루트 필드와 함께 "exports"
를 사용하고 있다면, 위에서 언급한 주요 변경 사항을 염두에 두어야 한다. Metro에서 이 기능을 활성화한 사용자의 경우, 모듈 해석 과정에서 "exports"
가 우선적으로 고려된다.
실제로, 사용자에게 가장 큰 변화는 "exports"
패키지 캡슐화를 준수함으로써 앱 내에서 접근할 수 없는 하위 경로에 대해 경고가 발생하는 것이다.
"exports"
로 마이그레이션하기
패키지에 "exports"
필드를 추가하는 것은 완전히 선택 사항이다. "exports"
를 사용하지 않는 패키지의 경우, 기존의 패키지 해결 기능은 동일하게 동작한다. 또한 이러한 동작을 제거할 계획도 없다.
"exports"
의 새로운 기능은 React Native 패키지 관리자에게 매우 유용한 도구가 될 것이다.
- 패키지 API를 강화한다: 이제 패키지의 모듈 API를 재검토하고, 내보낸 서브 경로 별칭을 통해 공식적으로 정의할 수 있다. 이를 통해 사용자가 내부 API에 접근하는 것을 방지하고 버그 발생 가능성을 줄일 수 있다.
- 조건부 내보내기: 패키지가 React Native for Web(예:
"react-native"
와"browser"
)을 대상으로 하는 경우, 이제 이러한 조건의 해결 순서를 패키지에서 직접 제어할 수 있다(다음 섹션 참조).
"exports"
를 도입하기로 결정했다면, 이를 주요 변경 사항으로 간주할 것을 권장한다. Metro 문서에는 플랫폼별 확장 기능을 대체하는 방법을 포함한 마이그레이션 가이드가 준비되어 있다.
Metro 구현의 관대한 동작에 의존하지 말자. Metro는 하위 호환성을 유지하지만, 패키지는 "exports"
가 명세에 문서화된 방식과 다른 도구에서 엄격하게 구현된 방식을 따라야 한다.
새로운 "react-native"
조건
커뮤니티 조건(conditional exports)으로 "react-native"
를 도입했다. 이는 "node"
와 "deno"
와 같은 인정받는 런타임과 함께 React Native 프레임워크를 나타낸다(RFC).
React Native 프레임워크(모든 플랫폼)와 일치한다. React Native for Web을 타겟팅하려면 "browser" 조건을 이 조건보다 먼저 지정해야 한다.
이 변경은 이전의 "react-native"
루트 필드를 대체한다. 이전에는 프로젝트에 따라 해결 순서가 결정되어 React Native for Web 사용 시 모호함이 발생했다. "exports"
아래에서는 _패키지가 조건부 진입점의 해결 순서를 명확히 정의_하므로 이러한 모호함이 제거된다.
"exports": {
"browser": "./dist/index-browser.js",
"react-native": "./dist/index-react-native.js",
"default": "./dist/index.js"
}
기존 플랫폼 선택 방법이 널리 사용되고 있으며, 프레임워크 간 동작이 복잡할 수 있기 때문에 "android"
와 "ios"
조건을 도입하지 않기로 결정했다. 대신 Platform.select()
API를 사용하길 권장한다.
미래: 기본값으로 활성화되는 안정적인 "exports"
다음 React Native 릴리스에서는 이 기능의 unstable_
접두사를 제거할 계획이다. 성능 개선 작업과 버그 수정을 마친 후 기본값으로 Package Exports 해석을 활성화한다.
"exports"
가 모든 사용자에게 활성화되면 React Native 커뮤니티를 한 단계 더 발전시킬 수 있다. 예를 들어 React Native의 핵심 패키지를 업데이트해 공개 모듈과 내부 모듈을 더 잘 분리할 수 있다.
감사의 말
React Native 커뮤니티 멤버들 중 RFC에 피드백을 제공해준 분들께 감사드립니다: @SimenB, @tido64, @byCedric, @thymikee.