lerna를 사용해 멀티 모듈 node 프로젝트를 구성한 뒤 lerna link convert를 실행하면 devDependencies가 root의 packages.json에 모이게 됩니다. 개별 모듈에는 런타임 의존성(package.json의 dependencies)만 남습니다.

이렇게 모든 서브모듈에서 동일한 개발 의존성을 갖도록 할 수 있지만 문제가 하나 생기는데, 기존에는 개별 모듈의 node_modules/.bin에 있던 실행 파일들이 사라지고 프로젝트 root의 node_modules/.bin에 해당 파일들이 생성된다는 것입니다.

node에서 의존성을 찾을 때 현재 디렉토리의 node_modules에 존재하지 않는 모듈은 상위 디렉토리의 node_modules에서 찾게 됩니다. 그러나 실행파일은 해당 모듈의 node_modules/.bin에서만 찾기 때문에 tsc, next 등을 실행할 수 없게 됩니다.

그래서 시도한 해결책은, root의 node_modules/.bin의 실행 파일들을 개별 모듈들이 복사해가기. 실행 파일들은 대부분 .js파일의 심볼릭 링크(Symbolic link)라서 그냥 복사해버리면 실행이 안됩니다. .js파일 하나만 덩그러니 생겨버리고 해당 파일이 의존하는 다른 모듈을 찾지 못하기 때문입니다.

따라서 그냥 복사하면 안 되고 심볼릭 링크 상태를 유지하면서 복사해야 합니다. 게다가 이 링크들은 상대 경로 (relative path)를 가지고 있기 때문에, 링크 상태를 유지하면서 복사하더라도 또 실행이 안됩니다. 상대 경로는 그대로 가져왔는데 해당 링크의 디렉토리는 달라졌으니 기존의 타겟 파일을 못 찾게 되는 것입니다.

고맙게도 superuser.com에 상대경로의 심볼릭 링크를 타겟을 잃지 않으면서 복사하는 방법이 있었고, 이를 바탕으로 상위 디렉토리의 node_modules/.bin에서 심볼릭 링크들을 복사해오는 쉘 스크립트를 작성했습니다.

# run in the directory which has node_modules.

ROOT_BIN_DIR=../../../node_modules/.bin
THIS_BIN_DIR=node_modules/.bin

mkdir -p node_modules/.bin;

for i in $ROOT_BIN_DIR/*; do
  ln -s ../../$ROOT_BIN_DIR/$(readlink $i) $THIS_BIN_DIR/${i##*/};
done

위 스크립트는 서브 모듈이 3단계 아래 있는 상태에서 작동하는 스크립트입니다. ${i##*/}/를 구분자로 삼아 변수 i의 값을 split한 뒤에 가장 마지막 토큰(즉 파일명)을 취한다는 의미입니다.

root/
  ├node_modules/.bin
  └dir1/
    └dir2/
      └here/
        └node_modules/.bin

위 스크립트를 root/dir1/dir2/here에서 실행하면 root/node_modules/.bin의 심볼릭 링크들을 타겟을 잃지 않으면서 복사해옵니다. 스크립트의 ROOT_BIN_DIR../../../부분을 해당 서브모듈의 depth에 따라 조절하면 됩니다.

lerna bootstrap으로 초기 의존성을 잡아준 뒤에 개별 모듈마다 위 스크립트를 실행해 root의 node_modules/.bin을 복사해오면 직면했던 문제가 해결됩니다. 깔끔.