배경
pnpm workspace 를 사용하는데, 다른 react 버전을 사용하는 두 개 이상의 package 를 포함할 경우 react 를 사용하는 프로젝트가 하나일 땐 발생하지 않던 type check 에러가 발생했다.
현상
-
pnpm workspace +
apps/web
(next 15.3.4 + react ^19.0.0) 에서는next dev
,next build
가 멀쩡하다.project_repo ├── apps │ └── web │ ├── package.json // next 15, react 19, etc. │ └── ... ├── package.json ├── pnpm-workspace.yaml // packages: apps/* └── ...
전체 folder structure
. ├── apps │ └── web │ ├── README.md │ ├── eslint.config.mjs │ ├── next-env.d.ts │ ├── next.config.ts │ ├── node_modules │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── src │ └── tsconfig.json ├── node_modules │ └── prettier -> .pnpm/prettier@3.6.2/node_modules/prettier ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml
-
거기에
apps/cms
(@strapi/strapi 5.16.1 + react ^18.0.0) 설치까지 하면 아래와 같은 에러가 터진다.. ├── apps │ ├── cms │ │ ├── package.json // @strapi/strapi 5, react 18, etc. │ │ └── ... │ └── web │ ├── package.json // next 15, react 19, etc. │ └── ... ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml // packages: apps/*
./src/app/page.tsx:7:10 Type error: 'Fragment' cannot be used as a JSX component. Its type 'ExoticComponent<FragmentProps>' is not a valid JSX element type. Type 'ExoticComponent<FragmentProps>' is not assignable to type '(props: any, deprecatedLegacyContext?: any) => ReactNode'. Type 'import("/Users/ndaemy/Documents/dev/poc-pnpm-workspaces-hoisting/node_modules/.pnpm/@types+react@19.1.8/node_modules/@types/react/index").ReactNode' is not assignable to type 'React.ReactNode'. Type 'ReactElement<unknown, string | JSXElementConstructor<any>>' is not assignable to type 'ReactNode'. Property 'children' is missing in type 'ReactElement<unknown, string | JSXElementConstructor<any>>' but required in type 'ReactPortal'. 5 | <div> 6 | {["a", "b", "c"].map((item) => ( > 7 | <Fragment key={item}> | ^ 8 | <p>{item}</p> 9 | </Fragment> 10 | ))} Next.js build worker exited with code: 1 and signal: null
전체 folder structure
. ├── apps │ ├── cms │ │ ├── README.md │ │ ├── config │ │ │ ├── admin.ts │ │ │ ├── api.ts │ │ │ ├── database.ts │ │ │ ├── middlewares.ts │ │ │ ├── plugins.ts │ │ │ └── server.ts │ │ ├── database │ │ │ └── migrations │ │ ├── favicon.png │ │ ├── node_modules │ │ │ ├── @strapi │ │ │ ├── @types │ │ │ ├── better-sqlite3 -> ../../../node_modules/.pnpm/better-sqlite3@11.3.0/node_modules/better-sqlite3 │ │ │ ├── react -> ../../../node_modules/.pnpm/react@18.3.1/node_modules/react │ │ │ ├── react-dom -> ../../../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom │ │ │ ├── react-router-dom -> ../../../node_modules/.pnpm/react-router-dom@6.30.1_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/react-router-dom │ │ │ ├── styled-components -> ../../../node_modules/.pnpm/styled-components@6.1.19_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/styled-components │ │ │ └── typescript -> ../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript │ │ ├── package.json │ │ ├── public │ │ │ ├── robots.txt │ │ │ └── uploads │ │ ├── src │ │ │ ├── admin │ │ │ ├── api │ │ │ ├── extensions │ │ │ └── index.ts │ │ └── tsconfig.json │ └── web │ ├── README.md │ ├── eslint.config.mjs │ ├── next-env.d.ts │ ├── next.config.ts │ ├── node_modules │ │ ├── @eslint │ │ ├── @tailwindcss │ │ ├── @types │ │ ├── eslint -> ../../../node_modules/.pnpm/eslint@9.29.0_jiti@2.4.2/node_modules/eslint │ │ ├── eslint-config-next -> ../../../node_modules/.pnpm/eslint-config-next@15.3.4_eslint@9.29.0_jiti@2.4.2__typescript@5.8.3/node_modules/eslint-config-next │ │ ├── next -> ../../../node_modules/.pnpm/next@15.3.4_react-dom@19.1.0_react@19.1.0__react@19.1.0/node_modules/next │ │ ├── react -> ../../../node_modules/.pnpm/react@19.1.0/node_modules/react │ │ ├── react-dom -> ../../../node_modules/.pnpm/react-dom@19.1.0_react@19.1.0/node_modules/react-dom │ │ ├── tailwindcss -> ../../../node_modules/.pnpm/tailwindcss@4.1.11/node_modules/tailwindcss │ │ └── typescript -> ../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ ├── src │ │ └── app │ └── tsconfig.json ├── node_modules │ └── prettier -> .pnpm/prettier@3.6.2/node_modules/prettier ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml
원인분석
- 터지는 에러가 React의 타입에 대한 에러이니
@types/react
에 대한 에러일 것이다. - 근데 그러면 monorepo 에서 버전이 다른 두 개의 라이브러리를 설치했을 때 그것들이 사용자의 의도와 무관하게 영향을 준다는 얘기인가?
이런 가설들을 세우고 해결방법을 찾아보게 되었다.
해결방법
-
- 요약:
.npmrc
에shared-workspace-lockfile=false
를 추가해서 lockfile 을 hoisting 하지 않도록 막으면 해결된다. - 문제: 이러면 pnpm workspace 의 장점이 많이 줄어든다.
node_modules
폴더의 사이즈도 비대해진다.
- 요약:
-
- 요약: 문제가 생기는 프로젝트의
tsconfig.json
에compileOptions.paths
에"react": ["./node_modules/@types/react"]
를 추가하면 해결된다. - 아쉬움: pnpm에서 파생된 생기는 이슈를 typescript 에서 해결하는 것에 대한 찝찝함이 있다.
- 요약: 문제가 생기는 프로젝트의
-
- 요약:
.npmrc
파일에 아래와 같이hoistPattern
을 설정하여@types/react
가 최상위로 호이스팅되는 것을 막는다.hoistPattern: - "*types*" - "!@types/react"
- 추가: 프로젝트 세팅에 따라 3번 해결책이 작동하지 않는 경우도 발생했다. 이 경우 2번 해결책을 사용하면 해결되었다.
- 요약: