라우터 구조를 잡으려는데 위와 같은 오류가 발생했다.
The inferred type of ‘router’ cannot be named without a reference to ‘.pnpm/@remix-run+router@1.18.0/node_modules/@remix-run/router’ . This is likely not portable. A type annotation is necessary.
타입스크립트가 router
의 타입을 추론할 수 없으니 타입 어노테이션이 필요하다는 오류였다.
먼저 타입스크립트가 타입추론을 제대로 하고 있는지 확인해야 한다.
위처럼 작성한 객체의 타입은 정상적으로 추론되는것이 확인되었다. 즉 타입스크립트는 정상적으로 타입 추론을 하고 있다.
'@remix-run/router'
모듈 임포트해당 패키지를 임포트 해준 후 타입 어노테이션을 추가해줬다.
Cannot find module ‘@remix-run/router’ or its correspoding type declarations
그랬더니 다음과 오류가 발생했다. 오류메세지를 보면서 임포트 경로가 잘못되었거나, 모듈이 없거나, 대응되는 타입 선언이 없다고 판단하며 하나씩 확인하기로 했다.
'@remix-run/router'
모듈 설치여부 확인패키지 경로를 따라가보니 `@remix-run/router
모듈이 정상적으로 설치되어 있었다. 처음에는 pnpm
의 자세한 원리를 몰라서 .pnpm
폴더 안에 있는 것이 문제라고 생각했다. 그래서 .pnpm
폴더에 대해 자세히 알아보기로 했다.
.pnpm
폴더와 내부 의존성 관리 확인npm
을 사용해 패키지를 설치하면, 각 패키지는 자신이 의존하고 있는 패키지를 node_modules
디렉토리의 하위 폴더에 설치한다.
project/
node_modules/
package-A/
node_modules/
lodash/ (4.17.15) // 중복 발생
package-B/
node_modules/
lodash/ (4.17.20) // 중복 발생
만약 내가 설치한 패키지들이 동일한 패키지를 의존하고 있으면 그 패키지는 중복해서 설치되는 문제가 발생하는 것이다. 게다가 트리 구조로 모든 의존성 패키지를 탐색/설치하는데에 시간 또한 오래걸린다.
pnpm
은 이러한 문제를 해결하기 위해 symlink된 node_modules
구조 방식 을 사용한다.
위의 사진처럼 pnpm
으로 설치한 패키지는 node_modules
아래에 설치된다. 일반적으로 설치된 패키지들은 각각의 의존성을 가진다.
이 때 npm
이나 yarn
은 내가 설치한 패키지가 의존하는 패키지도 같은 폴더에서 설치한다. 그래서 내가 직접 설치하지 않은 패키지라도 import 할 수 있다.
위 사진처럼 yarn을 사용하여 패키지 설치 시 패키지의 의존성까지 같은 디렉터리에 설치됨
하지만 pnpm
은 내가 설치한 패키지가 의존하는 패키지는 .pnpm-store
라는 중앙 저장소에 설치된다
.pnpm-store
에 설치된 의존성들은 node_module/.pnpm
에 하드링크를 걸어 생성된다.
즉 위 사진속의 패키지들의 중앙저장소인 .pnpm-store
에 설치되어 있고 .pnpm
폴더에서는 하드링크를 걸어 같은 파일을 참조할 수 있도록 해주는 것이다. 그리고 내가 설치했던 패키지들에 심볼릭 링크를 걸어 간접 의존성으로 가져온다. 그래서 내 프로젝트에서는 내가 직접 설치한 패키지에만 엑세스 할 수 있다는 것이다. 이전의 시도가 실패했던 이유는 이 때문이었다.
하드링크와 소프트링크
하드링크와 소프트링크는 리눅스 파일 시스템에서 파일을 참조하는 두가지 방법이다.
내용이 길어졌지만 결론적으로 .pnpm
에는 이상 없다는 것을 확인했으니 문제는 대응되는 타입선언이 없는 것이라고 판단했다.
declaration
컴파일 옵션 제거하여 선언파일의 생성을 막기타입스크립트 저장소에서 나와 동일한 문제를 겪고 있는 이슈를 발견했고 해당 이슈의 코멘트에서 문제의 원인을 찾을 수 있었다.
위 코멘트에서는 아래의 4가지 조건이 충족되면 이 에러가 발생한다고 했다.
declaration
옵션이 켜져 있으면, TS 컴파일러는 모든 .ts
소스파일을 컴파일할 때 해당 소스파일에 대한 선언 파일 .d.ts
를 생성한다. 이때 선언 파일은 타입 정보만 담고 있고 함수나 변수의 구현부가 없기 때문에 선언 파일에서는 타입 추론을 할 수 없다. 따라서 소스코드에서 타입추론을 사용해 간결하게 코드를 작성했을지라도 .d.ts
선언 파일을 생성할 때 명시적으로 타입을 지정해줘야 한다.
// app.tsx
const router = createBrowserRouter([
{ path: '/', element: <App /> },
{ path: '/login', element: <div>로그인</div> },
]);
export default router
즉 TS컴파일러가 내가 작성한 위의 코드를 .d.ts
로 변환하는 과정에서 react-router
가 의존하는 '@remix-run/router'
에서 지정된 router
타입을 명시해줘야 한다. 하지만 내 프로젝트는 react-router
에 대한 의존성만 가지고 있다. pnpm
을 사용하면 간접 의존성인 '@remix-run/router'
에서 타입정보를 직접 참조할 수 없기 때문에 타입추론이 안됐던 것이다.
// tsconfig.json
{
// 컴파일 옵션중 일부
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
...
},
}
나는 컴파일시 타입 선언파일을 생성해주는 declaration
옵션을 명시적으로 설정해주진 않았지만 composite
를 true
로 설정함으로써 자동으로 declaration
가 true
로 설정되어 발생한 오류였다. 내 프로젝트의 타입들은 다른프로젝트에서 사용될 일이 없기 때문에 선언파일이 필요 없어 해당 옵션을 제거해줬고 오류를 제거할 수 있었다.
해결법은 컴파일 옵션 한 줄을 지우는 것이 전부였지만, 이 과정에서 패키지 매니저가 의존성을 읽어오는 방법과 타입 추론이 어떻게 이뤄지는 지를 알 수 있어 많은 것을 배웠다고 생각한다.
패키지 매니저를 선택할 때 pnpm
이 의존성 트리가 평평하고 병렬설치를 해서 npm
에 비해 빠르다는 사실을 알고 pnpm
을 선택했지만, 왜 병렬설치를 하는지, 의존성트리가 어떻게 설치되는지 자세한 원리를 몰랐다.
지금이라도 pnpm
의 자세한 원리를 알게되면서 패키지의 설치 순서를 신경 쓸 필요 없기 때문에 패키지를 병렬 설치해 pnpm
이 빠르다는 사실과, 하나의 패키지를 여러곳에서 사용해도 로컬에서 참조하기 때문에 용량문제를 해결했다는 사실을 알게되어 다행이라고 생각한다.