[번역] ESM으로 패키지를 최신화한 경험기

Jisu Yuk
47 min readAug 28, 2023

--

원문 : https://blog.isquaredsoftware.com/2023/08/esm-modernization-lessons/

Redux 패키지를 ESM으로 마이그레이션하면서 겪은 고통스러운 경험과 힘들게 얻은 교훈에 대한 세부 정보를 공유합니다.

목차

  • 서론
  • Redux 패키지 배경 소개
    - 패키지와 설정
    - 이슈 히스토리
  • 초기 시도
    - Vitest로 마이그레이션
    - 초기 알파 테스트
  • 더 나은 설정에 대한 조사
  • 패키징을 위한 CI 설정
    - 초기 CI 설정
    - Are The Types Wrong?
  • 패키징 업데이트, 2 라운드
    - 빌드 툴 변경
    - UMD 빌드 아티팩트 변경
    - 웹팩 4 호환
    - Immer 10 베타
    - 타입스크립트 타입 선언
    - 2 라운드 결과
    - 기타 패키지 업데이트
  • Immer의 패키징 업데이트
  • Next.js 및 React 서버 컴포넌트에서의 문제
  • 불만 토로하기
  • 최종 생각
    - 현재 상황은 어떤가요?
    - 교훈과 핵심
    - 향후 작업
  • 추가 정보

서론

8년이 넘는 시간 동안 JS 생태계는 JS 코드를 배포하고 사용하기 위해 ES 모듈(“ESM”)을 사용하는 방향으로 서서히 전환해 왔습니다. Python 2에서 3으로의 전환과 유사하게, 이 전환은 매우 어렵고 고통스러운 과정이었습니다.

패키지 관리자로서 제 라이브러리가 가능한 한 다양한 환경에서 최대한 호환되어 사용될 수 있도록 하고 싶었습니다. 안타깝게도 이는 다양한 빌드 도구와 런타임 환경의 미묘한 차이와 동작 방식에 익숙해져야 한다는 것을 의미하기도 합니다.

올해 초에 저는 Redux 라이브러리 제품군의 패키지 설정을 업데이트하여 “완전한 ESM 호환성”을 제공하기 위한 작업을 시작했습니다. 마침내 합리적으로 잘 작동하는 것으로 보이는 일련의 설정을 생각해낸 것 같지만, 그 과정은 정말 힘들었습니다.

가장 큰 불만 중 하나는 “JS 패키지를 올바르게 배포하는 방법”에 대한 권위있고 포괄적인 가이드가 하나도 없다는 것입니다. 저는 이러한 가이드를 작성하고 게시할 수 있는 전문가에게 가이드 작성을 부탁해 왔습니다. 이상적이지만 어떤 파일 형식을 포함할지, ESM/CJS의 상호 운용을 위한 설정 및 package.exports를 구성하는 방법, TS 타입 버전 처리, 트리 쉐이킹 보장, 호환성 문제 확인, 특정 빌드 도구를 올바르게 지원하는 방법 등을 다룰 수 있어야 합니다. 아래에 링크할 예정인 몇 가지 가이드를 찾았지만 제가 원하던 범위와 내용에 완전히 부합하는 것은 없었습니다.

이 게시물은 “권위있는 가이드”가 아닙니다. 제가 시도한 방법과 그 과정에서 어렵게 얻은 교훈을 요약한 것입니다. 사람들이 이 글을 보고 “잘못하고 있다”고 말하는 횟수를 보면 제가 아직도 놓치고 있는 부분이 많을 것입니다 :) 하지만 이 정보가 완전히 포괄적이고 권위 있는 내용은 아니더라도 유용하고 유익한 정보가 되길 바랍니다.

ESM 사양의 역사, 현재의 혼란과 호환성 문제를 야기한 논쟁과 결정, 그리고 우리가 어떻게 이런 혼란에 빠지게 되었는지에 대해 이미 많은 글이 나와 있습니다. 이 글은 어느 정도 관리하기 쉬운 분량으로 유지하기 위해 몇 가지 링크를 찾아서 마지막에 나열하고, 이 과정에서 제가 겪은 경험과 단계에 주로 초점을 맞추려고 합니다.

Redux 패키지 배경 소개

패키지와 설정

2022년 말에 저는 아래의 패키지들을 Redux 조직의 일부로서 유지 관리하고 배포했습니다.

이러한 각 패키지에는 고유한 개발 히스토리, 패키징 설정 및 빌드 구성이 있습니다.

  • 모든 패키지에는 ESM, CJS 및 UMD 빌드 아티팩트가 포함되었습니다. 이는 임베디드된 process.env.NODE_ENV 값 또는 development 또는 production 버전으로 사전 컴파일된 다양한 조합으로 이루어져있습니다.
  • 모든 빌드 아티팩트는 .js 확장자를 사용했습니다.
  • 모든 패키지는 IE11 호환성을 위해 ES5 구문으로 트랜스파일되었습니다.
  • redux, react-redux, redux-thunk, reselect는 Babel로 트랜스파일되고 Rollup으로 번들링되었습니다. RTK는 번들링과 1차 트랜스파일링을 수행하는 커스텀 ESBuild 래퍼 스크립트를 사용하여 빌드되었지만, ES2015 코드를 ES5로 낮추기 위해 tsc도 사용했습니다.
  • 모든 패키지는 package.jsonmain, module, types 필드를 사용했습니다. 비교적 최신에 등장한 exports 필드를 로드할 빌드 아티팩트를 정의하기 위해서 사용하지는 않았습니다.
  • RTK 산출물을 제외한 대부분의 패키지는 유형별로 빌드 아티팩트를 다른 폴더로 내보냈습니다. (UMD의 경우 dist, CJS의 경우 lib, ESM의 경우 es)

몇가지 package.json 파일 예제입니다.

{
"name": "redux",
"version": "4.2.1",
"main": "lib/redux.js",
"unpkg": "dist/redux.js",
"module": "es/redux.js",
"typings": "./index.d.ts",
"files": ["dist", "lib", "es", "src", "index.d.ts"]
}
{
"name": "react-redux",
"version": "8.0.5",
"main": "./lib/index.js",
"types": "./es/index.d.ts",
"unpkg": "dist/react-redux.js",
"module": "es/index.js",
"files": ["dist", "lib", "src", "es"]
}
{
"name": "@reduxjs/toolkit",
"version": "1.9.5",
"main": "dist/index.js",
"module": "dist/redux-toolkit.esm.js",
"unpkg": "dist/redux-toolkit.umd.min.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/package.json",
"src/",
"query"
]
}

RTK의 설정은 3개의 개별 진입점인 @reduxjs/toolkit, @reduxjs/toolkit/query, 그리고 @reduxjs/toolkit/query/react 때문에 더 복잡했습니다. RTK의 package.json에는 두 개의 RTKQ 진입점이 나열되지 않았습니다. 대신, 배포된 패키지에는 실제 /query 폴더가 있었고, /query/package.json/query/react/package.json 파일이 차례로 dist 폴더의 올바른 아티팩트를 가리키고 있었습니다. (이 설정은 “웹팩 및 다른 몇 가지 도구와 함께 작동하는 것 같다고 생각합니다”라는 실험의 결과였습니다. )

나머지 이야기와 직접적인 관련은 없지만, 배포 프로세스의 일부로 사용하는 매우 유용한 도구 두 가지를 소개해드리고 싶습니다.

  • release-it: Git 태그 지정 및 푸시를 포함하여 NPM에 배포하는 실제 단계를 자동화합니다.
  • yalc: 패키지 전체에 대해 로컬 “배포”를 수행하여 예제 프로젝트에 설치하는 것을 테스트할 수 있습니다. 심볼릭 링크(예: npm link) 관련 문제를 방지하고 실제 빌드 및 배포 단계를 테스트할 수 있습니다.

이슈 히스토리

2021년 중반, 클라이언트 코드와 서버 코드에서 동시에 RTK를 제대로 로드할 수 없다는 문제가 보고되었습니다. 2022년 초에도 비슷한 문제가 보고되었는데, package.jsonmodule을 사용했지만 exports 필드가 없기 때문에 .mjs ESM 파일에서 RTK를 올바르게 임포트할 수 없다”는 것이었습니다. 마지막으로, RTK가 TypeScript의 새로운 moduleResolution: “node16” 옵션에서 동작하지 않는 문제도 보고되었습니다.

이전에 패키지에 exports를 추가하는 것이 어떤 영향을 미치는지 물어본 적이 있었는데, “이는 중대한 변경에 해당한다”는 답변을 들었습니다. 즉, 각 패키지의 다음 주요 릴리스가 나올 때까지는 이 기능을 고려할 수 없다는 뜻이었습니다. 하지만 메이저 릴리스가 언제 나올지 전혀 예상할 수 없었습니다. Redux 4.0은 2018년에, RTK 1.0은 2019년 말에 출시되었습니다. React-Redux 8.0은 더 최근인 2022년 중반에 출시되었습니다.

Redux 코어는 실제로 2019년에 TS로 전환되었지만, 아직 배포되지 않았습니다. 4.x와 수작업으로 작성된 타입 정의는 올바르게 작동하고 있고, 5.0 메이저 버전 출시로 인한 잠재적인 생태계 이탈에 대한 우려가 있었기 때문입니다. 또한 RTK와 관련된 기능 작업도 많았습니다.

2022년 11월에 RTK 1.9를 출시했습니다. 두 달 정도 휴식을 취한 후 마침내 패키지를 최신화하는 작업을 본격적으로 시작하게 되었습니다.

초기 시도

언젠가는 이런 날이 올 것이라는 생각에 지난 몇 년 동안 “모던 JS 배포”, ESM, ESM/CJS 상호 운용에 관한 글들을 모아뒀습니다. 최종 확인 결과, 목록에 약 175개의 글이 있었습니다!

저는 첫 단계로서 그중 몇 가지 글들을 검토했습니다. 제가 읽은 내용을 바탕으로 결론을 내렸습니다.

  • package.json 파일에 "type": "module"을 추가하여 Node와 번들러가 패키지에 ESM 파일이 포함된 것으로 감지할 수 있도록 해야 했습니다.
  • 또한 package.json”exports” 키를 추가하고, 내부에 가능한 진입점과 다른 환경에서 임포트할 때 사용할 빌드 아티팩트를 나열하는 키를 추가해야 했습니다.

파일 확장자로 .mjs를 사용하여 Node가 특정 파일을 ES 모듈로 인식하도록 하는 방법에 대한 언급을 본 적이 있습니다. 솔직히 말해서, 저는 그 확장자가 보기 흉하고 우스꽝스러워 보였기 때문에 그 확장자를 전혀 사용하고 싶지 않았습니다.

첫 번째 시도는 RTK PR #3095: RTK 패키지를 전체 ESM으로 마이그레이션이었습니다. PR 설명에 따르면 여기에는 다음과 같은 변경 사항이 포함되어 있습니다.

이 PR은 RTK 패키지를 기존의 “ESM 및 CJS 모듈을 포함하지만 완전한 ESM은 아님”에서 {type: “module”}을 사용하여 완전한 ESM으로 변환하고 CJS를 계속 지원합니다.

— BREAKING: 기본 RTK package.json 파일을 {type: “module”}로 설정

— BREAKING: exports를 사용하여 타입, ESM 파일 및 CJS 파일을 가리키도록 모든 package.json 파일을 업데이트합니다.
- 어떤 도구에서 누가 사용하게 될지 알 수 없기 때문에 여전히 mainmodule은 존재합니다.

— 빌드 스크립트를 업데이트합니다.
- 실행 시 ESM 호환을 위해 __dirname 사용을 교체하고 Terser import를 수정했습니다.
- 모든 빌드 타겟을 esnext로 전환하여 TS 트랜스파일 이외의 산출물은 건드리지 않도록 했습니다.
- 모든 CJS 빌드 아티팩트를 ./cjs/ 폴더의 각 진입점에서 한 단계 더 깊숙이 중첩되도록 이동했습니다.(예: ./dist/query/cjs/)
- 각 폴더의 패키지 파일에 {type: “commonjs”}를 추가했습니다.
- 일단 UMD 빌드 아티팩트는 포함되지 않습니다.

package.json은 다음과 같습니다.

{
"name": "@reduxjs/toolkit",
"version": "2.0.0-alpha.1",
"type": "module",
"module": "dist/redux-toolkit.modern.js",
"main": "dist/cjs/index.js",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/redux-toolkit.modern.js",
"default": "./dist/cjs/index.js"
},
"./query": {
"types": "./dist/query/index.d.ts",
"import": "./dist/query/rtk-query.modern.js",
"default": "./dist/query/cjs/index.js"
},
"./query/react": {
"types": "./dist/query/react/index.d.ts",
"import": "./dist/query/react/rtk-query-react.modern.js",
"default": "./dist/query/react/cjs/index.js"
}
}
}

Vite, CRA4/5, Next, Node에서 로컬 빌드를 테스트하고 publint를 실행해 보았습니다. 대부분 로컬에서 작동하는 것 같았기 때문에 CI에서 어떤 일이 발생하는지 알아보기 위해 PR을 올렸습니다.

그리고 모든 것이 폭발했습니다!!!! 💣💣💣

몇 시간 더 이것저것 만지작거리다가 결과를 보고했습니다.

음… 좋은 소식은 런타임 코드가 작동한다는 것입니다.

나쁜 소식은 Jest가 골칫거리라는 것입니다. 특히, redux-thunk를 임포트하는 방식에 문제가 있어서 default export가 미들웨어 함수가 아닌 {default}와 같은 객체를 가져오기 때문에 스토어를 생성하려고 할 때 테스트가 폭발합니다.

저는 지난 몇 시간 동안 thunk의 export를 해킹하고 로컬에 다시 배포하는 데 시간을 보냈습니다. default export를 전혀 사용하지 않도록 thunk 패키지를 전환하면 어느 정도 도움이 되었지만, 이제 “프로덕션에 개발 미들웨어 없음” 테스트가 실패하고 있습니다.

그래서 PR을 닫았습니다. 아직 이 브랜치를 빌드할 수 없습니다.

수정

어제 제가 중단한 부분은 아래와 같습니다.

redux-thunk에는 default export가 있고, 그로인해 Jest가 계속 실패합니다.

— ESM으로 변환하려고 시도한 redux-thunk@3.0.0-alpha를 배포했지만 여전히 default export가 포함되어 있었습니다. 이는 Next로 빌드하는 데는 도움이 되었지만 로컬 Jest 테스트에는 도움이 되지 않았습니다.

— 그런 다음 default export를 삭제하고 named export만 있는 로컬 전용 redux-thunk를 배포했습니다. 실제로 도움이 되는 것 같았지만, getDefaultMiddleware에 대해 NODE_ENV=production 동작을 검증하는 RTK 테스트 중 하나가 실패했습니다.

Reactiflux의 일부 직원들은 Vitest로 전환하는 것을 검토해 볼 가치가 있다고 제안했습니다. 저는 다른 테스트 러너로 마이그레이션하는 것이 제 우선 순위가 아니고 시간을 할애하고 싶은 일이 아니기 때문에 가능하면 그렇게 하지 않는 것이 좋다고 생각했습니다. 하지만 이 문제를 해결하는 것에는 도움이 될 수도 있다고 생각했습니다.

Vitest로 마이그레이션

전체 테스트 설정을 Jest에서 Vitest로 마이그레이션하는 데 시간을 낭비하고 싶지 않았습니다. 하지만 Vitest가 훨씬 더 빠르게 실행되고 ESM 지원이 더 뛰어나다는 등 긍정적인 평가를 많이 들었습니다.

며칠 후 저는 한 번 사용해 보기로 결정했습니다. 놀랍게도 전환은 매우 간단했습니다.

결국 RTK PR #3102: Jest에서 Vitest로 RTK 테스트 스위트 마이그레이션을 작업했습니다.

주요 테스트 설정은 의미가 있었고 jest.fn() -> vi.fn() 전환은 간단했습니다. 제가 겪은 가장 큰 어려움은 redux 패키지를 목킹하여 configureStore가 코어 라이브러리를 호출하는지 확인하려고 할 때였습니다. 테스트가 작동할 때까지 vi.mock()을 여러 번 조작해야 했습니다. 반면에 타이머 동작은 더 일관되게 작동하는 것처럼 보였습니다.

이 과정의 일부로 저장소에 있는 Jest 설정 파일이나 확장자가 .js인 빌드 스크립트같은 다른 보조 파일도 ESM 구문으로 변환해야 한다는 것을 알게 되었습니다.

PR이 통과되고 며칠 후에 병합할 수 있었습니다.

초기 알파 테스트

1월 21일에 @reduxjs/toolkit@2.0.0-alpha.1을 배포했습니다. 여기에는 RTK 패키징 변경 사항과 redux-thunk와 유사한 변경 사항이 포함되었으며, 빌드 아티팩트를 최신화하여 더 이상 JS 구문을 트랜스파일하지 않고 IE11 호환을 삭제했습니다.

물론 기대했던 것만큼 잘 작동하지 않았습니다 :)

Mateusz Burzyński(@andaristrake)는 Emotion과 Preconstruct를 비롯한 여러 라이브러리를 관리하고 있으며, 타입스크립트 컴파일러 작업에 취미로써 많은 시간을 할애하고 있습니다. 그는 패키징 설정의 복잡한 분야에 대한 전문가입니다.

제가 트위터에서 alpha.2를 발표했을 때 Mateusz는 몇 가지 수정 사항을 제안하며 답장을 보냈습니다(RTK 2.0과 Redux 코어 5.0 알파를 모두 살펴보고 있었습니다).

exports 맵에도 없는 dist/es/redux.mjs가 무엇인지 현재로서는 알 수 없습니다.

이와 같은 타입 조건을 사용하면 TS는 CJS에서 로드 및 필요로 하더라도 항상 모듈이라고 가정할 수 있습니다. default export가 없으므로… 아마 괜찮을 것입니다.

module 조건을 포함하고 ./dist/es/index.js를 가리키면 파일 형식(cjs vs esm)에 관계없이 번들러가 패키지를 한 번만 로드할 수 있습니다.

dist 파일에서 process.env.NODE_ENV를 제거하고 development/production 조건을 사용하여 이 작업을 수행합니다(프로덕션은 기본값으로 설정하고 개발은 옵트인하는 것이 가장 좋습니다).

참고용으로 이슈로 저장해 두었습니다.

얼마 지나지 않아 alpha.1/2의 설정 문제에 대한 새로운 이슈가 두 건 연속으로 접수되었습니다.

reduxjs/toolkit@2.0.0-alpha.2 에서 무언가를 가져올 때 tsconfig.json의 moduleResolutionnode16 또는 nodenext로 설정되어 있으면 타입스크립트가 타입을 확인할 수 없습니다. import 시 확장자 .js를 추가하면 문제가 해결되는 것을 발견했습니다.

알파의 현재 구성은 패키지 설정의 ”type”: “module”에도 불구하고 .js를 사용하여 CJS 모듈을 참조하기 때문에 최신 버전의 Node 또는 Node의 모듈 해석 스펙을 따르는 모든 도구에서 CJS 번들을 사용할 수 없습니다. ”type”: “module”로 설정된 경우 ”exports”.cjs가 필요합니다.

일부 번들러는 이 문제를 해결하고 더 관대하게 동작하지만(이것이 좋은 것인지 아닌지는 완전히 다른 문제입니다), 이 구성은 Node 또는 Node의 해석 메커니즘을 따르는 환경에서는 작동하지 않습니다.

저는 만족스럽지 않았습니다…

솔직히 문제를 제기해 주셔서 감사하지만, 이 모든 상황이 얼마나 엉망인지에 대해 화가 나는 것도 사실입니다 :(

저는 사용자에게 올바르게 행동하고 사용자가 앱에 사용할 것으로 예상되는 다양한 빌드 도구와 환경을 지원하고 싶습니다.

하지만 모든 글과 사람들이 제가 해야 할 일에 대해 모순된 지침을 제공한다면 그렇게 할 수 없습니다 :(

무슨 일이 일어나고 있는지 파악하고 가능한 오류를 잡아내는 데는 많은 노력이 필요했습니다.

더 나은 설정에 대한 조사

이와 동시에 Devon Govett은 트위터를 통해 React-Aria 패키지 업데이트에서 Node+ESM 지원을 개선한다는 소식을 전했습니다.

저도 비슷한 노력을 기울이고 있다고 답장하고 Mateusz를 태그했습니다. 얼마 지나지 않아 문제가 제기되고, 링크가 연결되고, 불만이 제기되는 것을 보았고, Mateusz는 다시 type: “module” 제거를 제안했습니다.

저는 답답한 마음에 그의 추천 사항에 대한 자세한 내용을 담은 전체 블로그 게시물을 게시해 달라고 간청했습니다. 대신 그는 전화 통화를 통해 직접 이야기를 나누자고 제안했습니다.

2월 27일, Mateusz와 저는 Nathan Bierema(Redux 개발자 도구 관리자)와 함께 통화에 참여했습니다. 토론 내용을 요약해서 저장해 두었습니다.

Mateusz는 ESM과 CJS를 어떻게 사용할 수 있는지에 대해 많은 생각을 쏟아냈고, ESM을 출시하는 것이 과연 합당한지에 대해서도 의문을 제기했습니다. 전반적으로 유용한 정보였지만 다음 단계에 대해서는 여전히 혼란스러웠습니다.

그 트위터 토론 중 어딘가에서 타입스크립트의 새로운 moduleResolution: “bundler” 옵션을 구현한 타입스크립트 팀원인 Andrew Branch (@atcb)와 연락이 닿았습니다. 2월 28일에 통화를 하기로 했습니다.

Andrew는 ESM의 작동 방식, TS가 ESM 및 모듈 import 경로를 처리하는 방법, Node 및 기타 도구가 파일이 실제로 ESM인지 확인하는 방법에 대해 설명해 주었습니다.

이에 대한 요약은 대략 다음과 같습니다.

  • type: “module”을 추가하면 확장자가 .js인 모든 파일은 ESM으로 해석됩니다. .cjs 파일은 CommonJS로 해석됩니다.
  • type: “module”이 없는 경우 .js 파일은 CommonJS로 취급됩니다. .mjs를 사용하여 개별 파일을 ESM으로 표시할 수 있습니다.

또한 TS 타입 정의를 미리 번들링할지 아니면 배포된 패키지에 someSourceFile.d.ts 파일로 남겨둘지에 대한 논의도 있었습니다.

패키징을 위한 CI 설정

초기 CI 설정

저는 PR CI 검사 중에 패키징에서 발생할 수 있는 오류를 포착하기 위해 각각 다른 빌드 도구를 사용하는 일종의 예제 애플리케이션 배터리를 구성해야 할 것이라고 오랫동안 생각했습니다.

alpha.2 이슈가 보고된 후, 저는 마지못해 실제 패키지 구성에 대한 작업을 더 하기 전에 이러한 CI 검사를 설정하는 데 시간을 할애해야 한다는 결론을 내렸습니다.

초기 테스트의 일환으로, 저는 로컬에서 RTK의 모든 진입점를 실행하는 작은 예제 앱을 만들었습니다. 여기에는 코어에서 configureStorecreateSlice를 실행하는 카운터, UI에 구애받지 않는 RTKQ createApi 엔드포인트, 리액트에 특화된 RTKQ createApi 엔드포인트가 있었습니다. 저는 이를 여러 프로젝트 설정에 붙여넣었습니다.

우리는 이미 현재 PR의 패키지 내용이 포함된 tarball을 미리 빌드하도록 CI를 설정해 두었고, 이를 사용하여 소스 코드 대신 PR 패키지 버전에 대한 단위 테스트를 실행하고 있었습니다. 저는 이를 확장하여 다른 앱도 해당 PR 빌드에 대해 테스트하기로 결정했습니다.

처음 몇 개의 예제 프로젝트를 새 $REPO/examples/publish-ci/ 폴더에 복사했습니다. 그리고 /publish-ci/ 내부의 폴더 이름을 매트릭스화하고, 각 예제에 PR 빌드를 설치한 다음 빌드 및 테스트하도록 GH 액션 워크플로우를 업데이트했습니다.

또한 앱이 실제로 올바르게 실행되는지 확인하기 위해 페이지 콘텐츠를 확인하여 카운터를 변경할 수 있는지, 두 API 엔드포인트가 올바른 모의 데이터를 가져왔는지 확인하는 작은 Playwright 테스트도 작성했습니다.

실제로 master 브랜치의 1.9.x에 대한 PR을 타겟팅하여 기존 패키지 설정에서 어떻게 작동하는지 먼저 확인했습니다.

결국에는 아래와 같은 예제 앱을 만들 수 있었습니다.

  • CRA 4 (웹팩 4)
  • CRA 5 (웹팩 5)
  • Next.js (웹팩 5)
  • Vite 4
  • CJS 및 ESM 모드의 Node

확인해야 할 다른 빌드 도구 조합도 많지만, 이 정도면 좋은 시작입니다.

Are The Types Wrong?

이 과정에서 Andrew Branch가 Are The Types Wrong이라는 도구를 만들었다는 것을 알게 되었습니다. 이 도구는 배포된 NPM 패키지 버전을 선택하거나 .tgz 파일을 업로드하면 패키지 exports를 분석하여 타입스크립트가 설정을 해석하는 방식과 JS 파일과 TS typedef가 올바르게 일치하는지 여부를 확인할 수 있는 웹사이트입니다. 그런 다음 감지된 모든 진입점을 표시하고 불일치 및 오류에 대한 세부 정보를 보고합니다.

RTK 2.0.0-alpha.2에 대한 리포트입니다. https://arethetypeswrong.github.io

RTK의 모든 진입점을 감지했으며, 대부분의 진입점 + moduleResolution 조합이 정상적으로 보이는 것을 볼 수 있습니다. 하지만 moduleResolution: “node16” 그리고 일부 CJS 환경에는 여러 가지 문제가 있는 것으로 보입니다.

저는 이 분석을 RTK의 CI에 사용하여 향후 PR 변경 사항이 실제로 제대로 작동하는지 확인하고 싶었습니다. attw 저장소를 살펴보니 Andrew가 이를 코어 패키지와 웹사이트 패키지로 나눈 것을 알 수 있었습니다. 하지만 코어 로직은 아직 패키지로 게시되지 않았습니다.

처음에는 attw 리포지토리를 복제하는 CI 작업을 설정하고 핵심 로직을 가져와 PR 빌드 아티팩트를 분석하는 명령줄 스크립트를 작성해 보았습니다. 기술적으로는 효과가 있었지만 다행히도 Andrew를 설득하여 로직을 실제 @arethetypeswrong/core 패키지로 게시하도록 할 수 있었습니다.

거기에서 핵심 attw 로직을 실행하고, 보고서를 수집하고, 웹사이트의 디스플레이와 일치하도록 콘솔 테이블로 작성하는 CLI 스크립트를 작성했습니다. 이 작업은 ink 리액트 CLI 렌더러를 사용하여 수행했습니다(콘솔에서 테이블을 렌더링하는 데 너무 많은 시간을 소비했을 수도 있습니다). 결과는 꽤 괜찮았습니다.

그런 다음 예제 앱 빌드와 함께 또 다른 검사로 이를 호출하도록 RTK의 CI를 구성했습니다.

CLI 추가를 요청이 기존에 있었기 때문에 저는 제 것을 일시적인 대안으로 제공했습니다. 나중에 다른 누군가가 CLI 기능 추가 PR을 만들었습니다. 그 CLI는 이제 공식으로 배포되었고, 이제 제가 직접 만든 스크립트 대신 CI에서 그 CLI를 사용하도록 전환해야 합니다.

패키징 업데이트, 2 라운드

RTK와 의존성이 있는 작은 패키지를 먼저 업데이트하는 것이 최선이라고 판단했습니다.

이미 redux-thunk의 동작을 변경하기 위해 3.0-alpha.0을 배포해야 했습니다. 저는 추가 업데이트를 시도하기로 결정했습니다.

빌드 툴 변경

redux-thunk는 몇 가지 추가 TS 타입을 포함한 약 20줄 길이의 작은 단일 소스 파일 입니다. 따라서 패키징을 변경하기 위한 좋은 출발점이 되었습니다.

빌드 단계에서 여전히 Babel+Rollup을 사용하고 있다는 점에 주목하고, 대신 ESBuild를 사용하기로 결정했습니다. 하지만 어떻게 사용해야 할까요?

이미 RTK에 커스텀 ESBuild 래퍼 스크립트가 있었습니다. 이 스크립트를 redux-thunk 리포지토리에 복사하여 붙여넣는 것을 잠시 고려했지만, 너무 과하다는 결론을 내렸습니다.

이전에 다른 ESBuild 래퍼를 검색해본 적이 있습니다. 좀 더 살펴본 결과 tsup을 사용해 보기로 결정했습니다.

tsup은 실제로 꽤 잘 작동했습니다! 몇 시간 만에 제가 원하는 두 가지 아티팩트를 생성하는 간단한 tsup.config.ts 파일을 만들 수 있었습니다. 이 경우 thunk 코드에는 dev/prod 조건 검사가 없었기 때문에 각각 하나의 ESM 및 CJS 파일로 매우 간단하게 유지했습니다.

또한 type: “module”도 제거하고, 아티팩트에 .mjs.cjs를 사용하도록 전환하여 ESM 또는 CJS를 적절하게 강제 적용했습니다.

아래와 같이 thunk의 package.json 파일을 업데이트했습니다.

{
"name": "redux-thunk",
"version": "3.0.0-alpha.1",
"main": "dist/cjs/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"default": "./dist/cjs/index.cjs"
},
"./extend-redux": {
"types": "./extend-redux.d.ts"
}
}
}

한편, RTK 리포지토리와 동일하게 Jest에서 Vitest로 전환했습니다.

UMD 빌드 아티팩트 변경

또한 UMD 파일을 계속 유지할 가치가 있는지 여부에 대해 많은 시간 토론했습니다. redux-thunk는 주로 스크립트 태그로 사용하기 위해 UMD 번들과 함께 제공되었습니다(대부분 CodePens 또는 유사한 예제에서 수행되고 있다고 생각했습니다).

저는 지난 몇 년 동안 UMD 빌드를 계속 게시할 것인지에 대해 반복적으로 질문했습니다.

실제 조언과 답변에서 가장 유사했던 것은 다음과 같습니다.

  • Fred K Schott: “HTML 예제나 코드 편집기는 더 이상 UMD를 사용해야 할 이유가 되지 않습니다. 예를 들어 코드펜은 Skypack 통합 기능이 내장되어 있습니다.”
  • Marvin Hagemeister: “UMD를 건너뛰어도 괜찮다고 생각합니다. 모든 번들러는 ESM을 사용할 수 있으며 https://esm.sh같은 사이트를 통해 브라우저에서 쉽게 사용할 수 있습니다.”

그래서 마침내 Redux 패키지에서 UMD 빌드를 삭제할 때가 되었다고 결정했습니다 :)

이후에는 UMD를 대체할 수 있는 가장 좋은 방법은 프로덕션 모드로 미리 컴파일되고 더 이상 process.env.NODE_ENV 참조가 없는 다른 ESM 형식의 빌드 아티팩트를 포함시켜 브라우저에서 안전하게 로드할 수 있도록 하는 것이라고 생각했습니다. 이렇게 하면 사람들이 이를 <script type=”module”>로 사용하고 Unpkg 또는 유사한 CDN에서 호스팅하는 패키지에서 로드할 수 있습니다.

(알파/베타 릴리스 노트에 UMD를 유지하는 사용 사례에 대한 피드백을 요청했지만 아직 단 한 건의 의견도 받지 못했습니다… pre-release 버전에 대해 피드백을 주는 사람이 거의 없다는 것은 우리 모두 이미 알고 있습니다 🤷‍♂️).

웹팩 4 호환

3월 초에 Dominik Dorfmeister(React Query의 유지보수자)의 트윗을 보고 Webpack 4가 여전히 Webpack 5보다 다운로드 수가 더 많다는 것을 알았습니다. 이는 일반적인 사용 뿐만 아니라 CRA 4, Storybook 6, Expo의 웹 타겟 등과 같이 내부적으로 Webpack을 사용하는 프로젝트들 때문입니다.

안타깝게도 Webpack 4는 패키지 “exports” 필드를 지원하지 않으며, optional chaining 구문으로 코드를 제대로 구문 분석할 수도 없습니다. 게다가, main 필드에 .mjs 파일을 넣는 것을 지양하며 대신 .js 확장자가 필요합니다.

이번 메이저 버전의 목표 중 하나는 JS 구문을 모두 트랜스파일링하지 않고 TS 타입만 트랜스파일링하며 완전히 최신화된 JS만 출시하는 것이었습니다. 하지만 저는 사용자들에게 즉시 사용할 수 있는 좋은 경험을 제공하는 것도 중요하게 생각합니다. 최신화된 코드만 배포하려고 하면 아직 Webpack 4를 사용하는 사용자에게 문제가 발생할 수 있다는 것이 분명했습니다.

그래서 마지못해 Webpack 4 호환을 위한 추가 아티팩트를 포함하기로 결정했습니다. ESM 모듈 형식을 ES2017 구문으로 트랜스파일하고 .js 확장자를 사용하여 main 필드에서 이를 가리키기로 했습니다.

Immer 10 베타

Immer 불변 업데이트 라이브러리의 작성자 Michel Weststrate는 지난 1월에 봄에 Immer 10을 작업할 계획이라고 밝힌 바 있습니다. 주요 업데이트 계획은 성능과 레거시 ES5 호환 중단에 관한 것이었습니다. 그러나 Immer는 default export와 named export를 모두 사용하는 등 몇 가지 유사한 패키징 문제도 있었습니다. Immer는 9.x patch 릴리스에서 exports를 추가했지만 많은 사용자가 불편을 겪었습니다. 이 변경 사항은 즉시 수정되었다가 반쯤 되돌려졌고, Immer는 이상한 패키지 구성을 갖게 되었습니다.

제가 1월에 default export와 named export가 모두 있는 패키지에서 문제가 발생한다는 댓글을 남겼기 때문에 Michel은 10.0에서 default export를 삭제하기로 결정했습니다.

Immer 10이 베타 버전으로 출시되자 저는 RTK 2.0 브랜치에서 해당 버전을 사용하도록 업데이트 했습니다.

타입스크립트 타입 선언

Redux 4.x는 수작업으로 작성된 TS 타입 정의 파일과 함께 제공됩니다. RTK 1.x에는 소스에서 생성된 소스 파일별 개별 TS typedefs 파일이 있습니다.

tsup에는 typedefs를 생성하기 위해 tsc를 호출하고 이를 하나의 파일로 묶어주는 dts: true 옵션이 있습니다. 이 옵션은 Redux에서는 잘 작동했지만 RTK에서는 문제가 발생했습니다. 결국 RTK에서 개별 소스별 typedef 파일을 사용하는 것으로 해결했습니다.

이 시점에서 패키지 구성은 are-the-types-wrong 검사를 통과했지만, moduleResolution: “node16”의 ESM 아티팩트에 대한 ”FalseCJS” 경고가 있었습니다.

이 문제에 대해 Andrew Branch와 몇 차례 논의했습니다. 문제는 기술적으로 내보내는 내용과 액세스 방식에 실제 차이가 있을 수 있기 때문에 “CJS 모드의 내 아티팩트”와 “ESM 모드의 내 아티팩트”에 대해 별도의 TS 타입 정의를 가져야 한다는 것입니다.

이 문제를 해결하기 위해 Andrew가 권장하는 접근 방식은 실제로 두 개의 서로 다른 TS 모듈 설정을 사용하여 tsc로 프로젝트를 두 번 컴파일하고 .mjs.cjs/js 아티팩트와 일치하도록 확장자가 .d.mts.d.ts인 서로 다른 두 개의 typedef 세트를 내보내는 것입니다.

안타깝게도 당시 제가 알고 있던 빌드 도구 중 이 기능을 기본적으로 제공하는 도구는 없었고, 99% 중복되는 typedefs를 배포해야 한다는 사실이 신경 쓰였습니다. 그래서 저는 적어도 당분간은 패키지에 대해 이 ”FalseCJS” 문제를 해결하지 않기로 결정했습니다.

Andrew Branch는 나중에 출력 형식당 하나의 typedefs 파일을 실제로 출력하도록 하는 tsup에 PR을 제출했습니다. 이 글을 쓰는 시점에서 아직 직접 시도해 보지는 않았지만 나중에 한 번 시도해보고 어떤 결과가 나오는지 살펴보겠습니다.

Andrew는 현재 TS가 모듈 형식을 해석하는 방법에 대한 매우 긴 WIP 종합 문서 gistTS가 발견할 수 있는 모든 문제를 문서화한 are-the-types-wrong 리포지토리의 문서를 정리해 놓았습니다.

2 라운드 결과

저는 4월 초에 redux@5.0.0-alpha.4@reduxjs/toolkit@2.0.0-alpha.4 에 실제로 “올바른” 패키지 설정을 모두 배포했습니다.

최종적인 설정은 다음과 같습니다.

  • Redux
{
"main": "dist/cjs/redux.cjs",
"module": "dist/redux.mjs",
"types": "dist/redux.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/redux.d.ts",
"import": "./dist/redux.mjs",
"default": "./dist/cjs/redux.cjs"
}
}
}
  • Redux Toolkit
{
"module": "dist/redux-toolkit.legacy-esm.js",
"main": "dist/cjs/index.js",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/redux-toolkit.modern.mjs",
"default": "./dist/cjs/index.js"
},
"./query": {
"types": "./dist/query/index.d.ts",
"import": "./dist/query/rtk-query.modern.mjs",
"default": "./dist/query/cjs/index.js"
},
"./query/react": {
"types": "./dist/query/react/index.d.ts",
"import": "./dist/query/react/rtk-query-react.modern.mjs",
"default": "./dist/query/react/cjs/index.js"
}
}
}

또한 여기에 명시적으로 나열되어 있지는 않지만 브라우저 중심의 ESM 아티팩트도 포함했습니다. (더 확인은 해봐야겠지만, browser 또는 unpkg 키에 다시 추가할 수도 있습니다.)

또한 Webpack 4 호환의 일부로 RTK 패키지에 중첩된 진입점 package.json 파일을 유지했습니다.

이 설정은 제가 구성한 모든 샘플 프로젝트에서 빌드 및 실행되는 것으로 보였지만, React Native 프로젝트는 포함되지 않았습니다.

기타 패키지 업데이트

나중에 reselect@5.0.0-alpha.0 에 동일한 패키지 업데이트를 적용했습니다.

원래는 React-Redux의 메이저 버전 릴리스를 계획하지 않았습니다. 하지만 몇 가지 잠재적인 변경 사항을 확인한 후 패키징 업데이트를 포함하고, Redux 5의 TS 타입 변경 사항을 처리하고, React 18의 useSyncExternalStore를 기본적으로 사용하지 않기 위해 React-Redux v9.0 메이저 버전을 릴리스할 가치가 있다고 결론을 내렸습니다.

이 글을 쓰는 현재, React-Redux의 패키징 업데이트를 위한 PR이 열려 있지만 몇 가지 TS 문제에 부딪혀서 해결하지 못했습니다.

Immer의 패키징 업데이트

Immer 10 베타 버전에서 성능 테스트를 해본 결과 훨씬 빨라진 것을 확인했습니다. 하지만 왠지 Immer 10 베타가 Immer 9보다 더 큰 것 같다는 점도 발견했습니다. Immer 10 베타 PR 브랜치를 내려서 빌드 크기를 몇 가지 비교했습니다.

논의 결과, 증가의 일부는 Immer의 Map/Set 지원이 이제 기본적으로 활성화되어 있기 때문이며, Redux에는 필요하지 않고 RTK는 Immer의 가장 큰 사용자 중 하나이기 때문에 Michel은 이를 다시 되돌리기로 동의했습니다. 그러나 이러한 변경에도 불구하고 Immer 10 베타 버전은 여전히 Immer 9보다 더 컸습니다.

Immer의 빌드 시스템을 자세히 살펴본 결과, 거의 사용되지 않는 tsdx 빌드 툴 패키지를 여전히 사용하고 있으며 ES6 구문을 타깃으로 하고 있다는 사실을 알게 되었습니다. 이로 인해 수많은 폴리필과 죽은 코드가 추가되고 있었습니다. 또한 소스맵과 번들에서도 몇 가지 문제도 발견했습니다.

Redux와 RTK 빌드 구성을 업데이트하는 데 많은 시간을 할애했기 때문에 저는 이러한 변경 사항을 Immer로 포팅하기로 자원했습니다. 빠르게 개념 증명을 완료했고 Immer의 번들 크기가 눈에 띄게 줄어드는 것을 확인했습니다. 저는 이러한 Immer 변경 사항에 대한 PR을 올렸고, Michel은 이를 v10의 일부로 병합했습니다.

Next.js 및 React 서버 컴포넌트에서의 문제

5월 초, Next 13.4가 출시되었습니다. 가장 큰 특징은 React Server 컴포넌트를 기반으로 하는 새로운 “App Router”가 이제 안정적으로 프로덕션에 사용할 준비가 되었다는 것이었습니다. 그 일환으로 Next의 CLI는 기본적으로 앱 라우터와 /app 폴더를 활성화한 상태로 새 프로젝트를 생성하도록 설정되었으며, 문서가 업데이트되어 앱 라우터와 React 서버 컴포넌트를 기본으로 사용하도록 안내하고 있습니다.

곧 React-Redux가 제대로 작동하지 않는다고 불평하는 새로운 이슈들이 React-Redux와 Redux Toolkit에 접수되기 시작했습니다. 주요 보고 내용은 React.createContext()가 null이거나 useLayoutEffect가 RSC 환경에 존재하지 않아서 오류가 발생한다는 것이었습니다.

한편, Redux 공동 관리자인 Lenz Weber-Tronic은 직장에서 Apollo Client를 사용하면서 지난 몇 달 동안 클라이언트 측 상태 관리 및 데이터 불러오기 라이브러리를 RSC와 함께 사용하는 방법에 대해 연구했습니다.

그가 직면한 한 가지 문제는 서버와 클라이언트 모두에서 React 훅을 생성할 수 있는 RTK Query의 createApi를 사용하는 방법이었고, 그는 React에 이 문제를 처리하는 방법에 대한 제안을 요청하는 이슈를 제출했습니다.

그는 또한 Apollo와 Next 13을 통합하는 방법에 대한 긴 RFC를 작성했고, 나중에 실험적인 Apollo + Next 인터옵 패키지를 배포했습니다.

6월 중순, 한 Apollo 사용자가 Next 13.4.6-canary.2에서 Apollo가 중단된다는 문제를 보고했습니다. 이 문제는 곧 다른 카나리아 빌드에서 수정되었지만, 이 문제로 인해 길고 답답한 토론이 벌어졌습니다.

이 스레드에서 한 Next 개발자는 이제 ESM 아티팩트를 발견하여 가져오는 내용을 더 잘 분석할 수 있으며 서버 컴포넌트에서 클라이언트 코드를 사용하는 것이 이제 오류로 간주되고 있다고 보고했습니다. 그들은 이렇게 말했습니다. “아폴로 클라이언트에 서버 컴포넌트 솔루션을 사용하려면 서버 export만 포함하는 별도의 react-server export 조건이 필요합니다.”

그 글을 읽은 후, 저는 RTK 쿼리에는 UI에 구애받지 않는 코드와 React 훅 기반 코드가 혼합되어 있으며 사용자가 서버와 클라이언트 모두에서 createApi를 사용하고 싶을 수 있다는 점에 주목했습니다. 또한 RTK의 아티팩트를 미리 번들로 제공하고 있는데, 이러한 Next/RSC의 제약 조건을 충족하기 위해 아티팩트를 어떻게 더 분리해야 할지 모르겠다는 점도 지적했습니다.

React 팀의 오랜 수석 아키텍트이자 현재 Vercel에서 Next를 개발하고 있는 Seb Markbage가 답했습니다. “불필요한 코드 브랜치를 제외한 내부 구현의 최적화된 버전을 배포하기만 하면 API 사용자는 여전히 동일한 API를 사용할 수 있습니다.”

이에 대해 격렬한 논쟁이 벌어졌습니다. 특히 Apollo와 같은 많은 패키지가 여전히 exports 선언을 추가하지 않은 상태이기 때문에 Lenz는 전체 생태계에 잠재적으로 파괴적인 패키징 변경을 요구하고 있다고 지적했습니다. 정적 분석기를 속이기 위해 import * as React를 사용하라는 믿을 수 없을 정도로 엉뚱한 해결 방법을 제시한 Seb에게 깜짝 놀랐습니다.

저는 메시지를 읽고 매우 실망하여 이렇게 답했습니다, ”패키징을 업그레이드하기 위해 몇 달 동안 노력했는데, 이제 RSC 환경에서 코드가 깨지지 않게 하려면 더 많은 작업을 해야 한다고 하네요. 정말 힘듭니다.”

이 특정 스레드의 대화는 사라졌지만, 트위터와 Reddit의 RSC에 대한 수많은 토론 스레드와 의견이 일치했습니다.

몇 주 후, Lenz는 현재 React 및 서버 컴포넌트 논란에 대한 나의 견해라는 제목의 게시물을 올렸습니다. 이 글에서 그는 RSC가 매우 유용한 기술이라고 생각하지만, 다음과 같이 언급했습니다.

  • 우리가 사용자를 지원하는 것이 훨씬 더 어려워졌고, 사용자들은 더 많은 문제를 제기하고 있습니다.
  • React(와 Next)에 대해 이해해야 할 것이 훨씬 더 많아졌습니다.
  • 이제 React와 함께 작동하는 라이브러리를 유지 관리하고 게시하는 것이 훨씬 더 어려워졌습니다.
  • RSC와 use 훅의 상태에 대한 React 팀의 커뮤니케이션이 매우 부족하고 이것이 생태계에 어떤 영향을 미칠지에 대한 논의가 거의 없는 것 같습니다.

또한 라이브러리가 RSC 환경과 데이터 가져오기를 더 잘 처리하는 데 도움이 될 수 있는 여러 가지 가능한 API를 나열했습니다.

Lenz의 게시물은 문제와 제안 목록에 동의하고 공감을 표하는 많은 사람들로부터 강력한 긍정적인 피드백을 받았습니다.

하지만 이러한 API 제안이나 RSC와 협력하는 패키지를 올바르게 배포하는 방법에 대해 React 팀으로부터 실제 답변을 받은 적은 없었습니다. 다만 RSC와 관련된 공식 문서를 추가하기 위해 노력하는 React 조직 내 몇몇 사람들로부터 연락을 받았고, RSC, 마케팅 및 사용과 관련된 많은 커뮤니티 피드백을 전달할 수 있었습니다.

Next 문서에는 서드파티 컨텍스트 제공자를 use client로 래핑하는 방법을 다루는 페이지가 추가되었지만, 이는 임시방편처럼 느껴집니다.

불만 토로하기

이러한 변경 사항에 많은 시간을 할애하고 나니 추적해야 할 사항이 너무 많아서 상당히 답답했습니다.

4월 말, 저는 트윗을 올렸습니다.

2023년에 라이브러리를 배포할 때 염두에 두어야 할 사항

— 빌드 아티팩트 형식(ESM, CJS, UMD)

— dev/prod/NODE_ENV 빌드들을 매트릭스화 하기

— 소스당 번들 또는 개별 .js

exports 설정

— Webpack 4 제한

— TS moduleResolution 옵션

— 사용자 환경

— 번들러 간 동작 차이

— Node ESM/CJS 모드

— TS typedef 출력(번들? 개별? .d.ts 또는 .d.mts?)

— 엣지 런타임

— 그리고 이제 React의 새로운 use client 및 RSC 제약 조건

— upstream으로 사용하는 라이브러리 또한 이 모든 것들을 준수

정말 말도 안 되는 일이 벌어지고 있습니다 :(

이 모든 것이 가능한 설정 변경, 엣지 케이스, 런타임 환경, 충돌하는 제약 조건을 어떻게 모두 따라잡을 수 있을지 모르겠습니다.

그리고 이 작업을 수행하는 방법에 대한 실제 종합적인 가이드는 없습니다. 모두 다른 사람들을 따라하고 있을 뿐입니다.

사용자를 올바르게 대하고 가능한 한 많은 환경에서 작동하는 패키지를 배포하려고 노력하고 있지만, 이 문제를 처리하는 것은 매우 짜증나는 일입니다.

이 생태계에서 모든 곳에서 작동한다는 것은 기적과도 같은 일입니다.

신경이 쓰였죠. 이 트윗은 수십 건의 리트윗, 인용, 답글이 달리면서 꽤나 입소문이 났습니다. 또한 일부 뉴스레터에도 링크되었습니다.

RSC 변경에 대해 “사기가 저하된 느낌”에 대한 제 의견을 쓴 후, 저는 그 토론을 링크하는 또 다른 트윗으로 후속 조치를 취하고 불만을 토로했습니다.

React 생태계에서 활동하는 라이브러리 관리자로서, 저는 React 서버 컴포넌트를 둘러싼 혼란에 상당히 좌절하고 있습니다.

라이브러리 관리자에게 가해지는 고통과 선전되는 이점을 비교했을 때 그 이점이 가치가 있는지에 대해 의문이 들기 시작했습니다.

이 트윗 역시 꽤 널리 퍼져 많은 반응을 얻었습니다.

며칠 후, 다음 생각이 떠올랐습니다.

최근 제가 작성한 두 개의 트윗에서 라이브러리 관리자로서 JS 생태계 혼란(패키지 설정, RSC)에 대한 불만을 다룬 내용이 모두 입소문을 타서 아이러니하게 느껴집니다.

저는 평소 공개적으로 불평하지 않고 긍정적인 태도를 유지하려고 노력합니다.

다른 사람들도 그런 좌절감을 느끼거나 공감하는 것 같습니다.

한 응답자는 이렇게 말했습니다. “사람들은 여러분이 하고 있는 일 그리고 여러분이 생태계에 대해 얼마나 인내심을 가지고 있고 긍정적인지 알고 있다고 생각합니다. 무언가가 여러분을 좌절하게 만든다는 것은 뭔가 큰 일이 제대로 작동하지 않거나 잘못되었다는 것을 의미합니다.”

최종 생각

저는 보통 어떤 릴리스가 완료되거나 토론이 끝난 후, 그 이야기가 완성된 것처럼 느껴지고 구체적인 요점과 답변이 있을 때 이런 블로그 게시물을 작성하는 것을 좋아합니다. “RTK 2.0이 출시되었으며, 여기에는 정확히 모든 곳에서 작동하는 패키징 업데이트 세트가 포함되어 있습니다.”라고 말할 수 있다면 정말 좋겠죠.

안타깝게도 여기서는 그렇지 않습니다.

이번 여름은 일과 Redux, 개인 시간으로 매우 바쁜 여름이었고, 저는 약간의 번아웃을 겪고 있다는 것을 깨달았습니다. 하지만 6월 초에 React Summit에서 돌아와서 이 포스팅을 시작했고, 두 달이 지난 지금에야 마무리하게 되었습니다.

현재 상황은 어떤가요?

현재 상황은 다음과 같습니다.

교훈과 핵심

모듈 패키징과 퍼블리싱 유형에 관해서는 아직도 아는 것이 거의 없다고 여러 번 말씀드렸습니다. 실제로 지난 몇 년 동안, 특히 올해에는 이 문제를 해결하기 위해 많은 시간을 할애해야 했기 때문에 대부분의 JS 개발자보다 더 많이 알고 있다고 생각합니다. 하지만 엄청나게 복잡하고 상충되는 수많은 요구 사항과 빠르게 변화하는 추적해야 할 목록을 고려할 때, 이 모든 주제에 대해 제가 여전히 아무것도 모르는 사기꾼처럼 느껴지는 것은 충분히 이해할 수 있다고 생각합니다.

그렇다면 실제로 제가 배운 것은 무엇일까요?

  • 현재 대부분의 번들러와 빌드 도구에서 작동하는 것으로 보이는 패키지 구성 세트가 있고 유효한 ESM/CJS 패키징이 있는 것 같습니다.
  • TS 타입 정의를 올바르게 배포하면 그 위에 또 다른 수준의 복잡성이 추가됩니다.
  • JS 빌드 아티팩트를 미리 번들링하면 파일 임포트에 .js가 있어야 하는 잠재적인 문제를 피할 수 있고 트리 셰이킹에 문제가 없는 것처럼 보입니다…
  • 하지만 “RSC 시나리오를 위해 클라이언트 코드를 분할”하는 것과 같은 문제가 발생하면 다른 문제가 발생할 수 있습니다.
  • 시중에 나와 있는 다양한 도구와 제약 조건을 모두 따라잡는 것은 거의 불가능하며 그것들은 계속 변경됩니다.
  • 빌드 도구와 그 특징 또는 제약 조건에 대한 전체 목록은 없습니다. 몇 가지 리소스(Sokra의 인터롭 테이블, 번들러 세부 정보가 포함된 글, Jason Miller의 스프레드시트 등)가 있지만 특정 도구를 만족스럽게 사용하기 위해 구성해야 하는 사항을 이해할 수 있는 원스톱 쇼핑은 없습니다.
  • 마찬가지로 퍼블리싱 패키지에 대한 공식적이고 포괄적인 리소스도 충분하지 않습니다. 몇 가지 괜찮은 가이드(예: JS 라이브러리 패키징을 위한 최신 가이드 , React 라이브러리 가이드(초안) , 최신 JS 배포하기)를 찾았지만, 여전히 많은 지식이 단편적이고 흩어져 있으며 상충되는 것처럼 느껴집니다. 특히 다양한 종류의 사용 시나리오를 다루기 위해 어떤 패키지 형식 + 아티팩트를 제공해야 하는지에 대한 지침이 필요합니다.
  • 생태계에는 이 프로세스를 자동화하는 데 도움이 되는 더 나은 도구가 절실히 필요합니다.
  • tsup은 꽤 괜찮은 것 같고, 이중 ESM/CJS 패키지 배포에 도움이 된다고 주장하는 다른 도구도 몇 가지 보았지만 사용해 보지는 않았지만 이 중 많은 부분이 자동화될 수 있을 것 같다는 느낌이 듭니다.
  • 마찬가지로, 일종의 “다양한 빌드 도구에 대한 테스트용 서비스형 도구”가 필요한 것 같습니다. 다양한 빌드 도구로 여러 샘플 Redux 프로젝트를 설정해 보았는데, react-aria가 비슷한 기능을 하는 것을 보았지만, 이런 설정은 어떻게든 상품화할 수 있고 라이브러리 작성자가 자신의 패키지가 생태계 전체에서 올바르게 작동하는지 검증하는 데 도움이 될 것 같았습니다.
  • React 서버 컴포넌트는 유용한 개념이자 도구이지만, 이것은 분명 생태계의 많은 부분을 무너뜨릴 수 있는 큰 혼란인 것 같습니다. “클라이언트 React에 대한 것은 아무것도 바뀌지 않고 모두 추가되는 것”이라는 React 팀의 의견은 이해하지만, 동시에 사용자와 라이브러리 작성자가 처리해야 하는 정신적 오버헤드와 사용 사례의 복잡성이 엄청나게 증가한다는 것을 의미합니다.
  • React 팀에서 라이브러리 생태계에 패키지와 RSC를 다루는 방법에 대한 홍보는 사실상 없었습니다. 공정하게 말하자면, Dan + Andrew와 통화를 했고, Dan은 Lenz의 RFC를 검토했으며, 이슈에 대해 몇 가지 논의가 있었습니다. 하지만 클라이언트 import 시 오류가 발생하는 Next에 대한 공지나 경고는 없었고, “각 카나리아 버전에는 무엇이 있나요?”라는 표도 없었으며, “RSC 호환을 위한 라이브러리 작성자에 대한 안내” 게시물이나 문서도 게시되지 않았습니다.
  • 전반적인 생태계의 CJS/ESM 전환은 길고 지속적인 악몽이었으며, 조만간 끝날 기미가 보이지 않습니다. 얼마 전에는 “CJS가 자바스크립트를 해치고 있다”는 주장과 “CommonJS는 사라지지 않을 것이다”는 주장이 대립하는 두 개의 게시물을 보았습니다. 분명히 이것은 우리가 몇 년 동안 고심해야 할 문제입니다.

향후 작업

아직 Redux 툴킷 2.0을 마무리해야 하지만 시간이 좀 걸릴 것 같습니다. 모든 패키지가 패키징 변경 사항을 완전히 최신 상태로 유지하고, 서로의 버전을 상호 참조하며, 모든 TS 타입이 작동하는지 확인하는 것만으로도 많은 시간이 소요됩니다. 코드 관련 변경 사항도 더 있을 수 있지만, 메이저 버전에 무엇이 포함되어야 하는지 확실한 목록이 없습니다.

또한 모든 유지 관리자가 일, 동기 부여, 시간, 현실 생활의 균형을 맞추기 위해 노력하고 있기 때문에 현재 Redux 작업은 우선순위가 낮습니다.

지금까지 이 모든 문제를 해결하느라 지친 한 해였습니다. 저희는 여전히 주요 릴리스 세트를 출시할 계획이며(궁극적으로, 아직은 예정 날짜를 약속할 수 없습니다), 이러한 새 버전이 생태계에 도움이 되고 호환성을 크게 개선할 수 있기를 기대합니다.

이 모든 시간과 노력이 그만한 가치가 있었기를 바랍니다.

추가 정보

--

--