pnpm의 심볼릭링크 된 의존성에서 타입추론 오류 해결하기

경이·2025년 1월 20일
0

📌 문제상황

라우터 구조를 잡으려는데 위와 같은 오류가 발생했다.

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의 타입을 추론할 수 없으니 타입 어노테이션이 필요하다는 오류였다.


문제해결로 이동하기


시도 1. 타입스크립트 자체의 문제인지 확인

먼저 타입스크립트가 타입추론을 제대로 하고 있는지 확인해야 한다.

위처럼 작성한 객체의 타입은 정상적으로 추론되는것이 확인되었다. 즉 타입스크립트는 정상적으로 타입 추론을 하고 있다.


시도 2. '@remix-run/router' 모듈 임포트

해당 패키지를 임포트 해준 후 타입 어노테이션을 추가해줬다.

Cannot find module ‘@remix-run/router’ or its correspoding type declarations

그랬더니 다음과 오류가 발생했다. 오류메세지를 보면서 임포트 경로가 잘못되었거나, 모듈이 없거나, 대응되는 타입 선언이 없다고 판단하며 하나씩 확인하기로 했다.


시도 3. '@remix-run/router' 모듈 설치여부 확인

패키지 경로를 따라가보니 `@remix-run/router 모듈이 정상적으로 설치되어 있었다. 처음에는 pnpm의 자세한 원리를 몰라서 .pnpm 폴더 안에 있는 것이 문제라고 생각했다. 그래서 .pnpm폴더에 대해 자세히 알아보기로 했다.


시도 4. .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에는 이상 없다는 것을 확인했으니 문제는 대응되는 타입선언이 없는 것이라고 판단했다.


시도 5. declaration컴파일 옵션 제거하여 선언파일의 생성을 막기

타입스크립트 저장소에서 나와 동일한 문제를 겪고 있는 이슈를 발견했고 해당 이슈의 코멘트에서 문제의 원인을 찾을 수 있었다.

위 코멘트에서는 아래의 4가지 조건이 충족되면 이 에러가 발생한다고 했다.

  • pnpm을 사용함
  • TS 패키지가 선언파일을 만들어냄
  • TS 소스에서 변수 또는 함수 반환 타입에 대한 타입 추론이 사용됨
  • 그 변수/함수가 export 됨

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옵션을 명시적으로 설정해주진 않았지만 compositetrue로 설정함으로써 자동으로 declarationtrue로 설정되어 발생한 오류였다. 내 프로젝트의 타입들은 다른프로젝트에서 사용될 일이 없기 때문에 선언파일이 필요 없어 해당 옵션을 제거해줬고 오류를 제거할 수 있었다.



✏️ 느낀점

해결법은 컴파일 옵션 한 줄을 지우는 것이 전부였지만, 이 과정에서 패키지 매니저가 의존성을 읽어오는 방법과 타입 추론이 어떻게 이뤄지는 지를 알 수 있어 많은 것을 배웠다고 생각한다.

패키지 매니저를 선택할 때 pnpm이 의존성 트리가 평평하고 병렬설치를 해서 npm에 비해 빠르다는 사실을 알고 pnpm을 선택했지만, 왜 병렬설치를 하는지, 의존성트리가 어떻게 설치되는지 자세한 원리를 몰랐다.

지금이라도 pnpm의 자세한 원리를 알게되면서 패키지의 설치 순서를 신경 쓸 필요 없기 때문에 패키지를 병렬 설치해 pnpm이 빠르다는 사실과, 하나의 패키지를 여러곳에서 사용해도 로컬에서 참조하기 때문에 용량문제를 해결했다는 사실을 알게되어 다행이라고 생각한다.


📙 REF

profile
록타르오가르

0개의 댓글