태그 pnpm모노레포안드로이드React Native빌드
전체 글 보기

pnpm

pnpm 모노레포에서 앱 하나를 빌드했더니 다른 앱이 깨졌습니다

pnpm 모노레포에서 앱끼리 minSdk 가 다르면 node_modules 에 남은 네이티브 빌드 결과 때문에 손대지 않은 앱이 깨집니다. 에러가 가리키는 고침이 왜 틀렸는지, 무엇을 지우면 되는지 적었습니다.

2026년 10월 6일 4분

어제 pnpm 모노레포 안에 있는 꿈가게 앱의 릴리즈 빌드가 1분 34초 만에 실패했습니다. 그런데 그 앱에서는 빌드에 영향을 줄 만한 것을 아무것도 바꾸지 않았습니다. 바로 전에 빌드한 것은 같은 저장소의 다른 앱이었고, 범인은 그 빌드가 남기고 간 파일이었습니다.

에러는 내 앱의 최소 버전이 낮다고 말합니다

pnpm 이 깔아 둔 라이브러리를 앱이 C++ 로 엮는 단계(configureCMakeRelWithDebInfo[arm64-v8a])에서 멈췄고, 메시지는 이랬습니다.

User has minSdkVersion 24 but library was built for 26 [//react-native-reanimated/reanimated]

minSdkVersion 은 이 앱을 설치할 수 있는 가장 낮은 안드로이드 버전입니다. 24는 안드로이드 7.0, 26은 8.0입니다. 메시지를 그대로 읽으면 “애니메이션 라이브러리(reanimated)가 26 이상을 요구하니 앱도 26으로 올려라” 처럼 읽힙니다.

그런데 그렇게 고치면 안 되는 이유가 있었습니다. 꿈가게는 지금까지 24로 잘 빌드됐고, reanimated 버전도 바뀐 적이 없습니다. 앱 쪽 숫자를 올리면 빌드는 통과하겠지만 안드로이드 7 사용자를 이유 없이 잘라내게 됩니다. 라이브러리가 26을 요구하게 된 게 아니라, 26으로 빌드된 결과물이 어딘가 남아 있다고 보는 편이 맞았습니다.

pnpm 에서는 열한 개 앱이 라이브러리 한 벌을 같이 씁니다

이 저장소에는 엑스포(Expo) 앱이 열한 개 있습니다. pnpm 은 같은 버전의 패키지를 앱마다 따로 깔지 않고, node_modules/.pnpm 안에 한 벌만 두고 모든 앱이 그것을 가리키게 합니다. 각 앱에서 reanimated 의 위치를 찍어 보니 열한 개가 전부 같은 폴더를 가리켰습니다.

디스크를 아끼는 좋은 구조인데, 네이티브 라이브러리에서는 문제가 생깁니다. reanimated 같은 라이브러리는 앱을 빌드할 때 자기 C++ 코드도 같이 컴파일하고, 그 결과를 자기 폴더 안(android/build, android/.cxx)에 씁니다. 라이브러리 폴더가 한 벌이니 빌드 결과물도 한 벌입니다.

그리고 앱마다 최소 버전이 달랐습니다.

앱minSdk
약알림, 얼리버디, 슬립플로우26
꿈가게를 포함한 나머지 8개24 (엑스포 기본값)

마지막에 빌드한 앱의 숫자가 남습니다

어제 일어난 순서는 이렇습니다. 약알림(26)을 먼저 빌드했고, 그때 reanimated 가 26 기준으로 컴파일돼 그 폴더에 남았습니다. 이어서 꿈가게(24)를 빌드하자 그 결과물을 다시 쓰려다가, 24짜리 앱에 26짜리 라이브러리를 붙일 수 없다며 멈춘 것입니다.

이 숫자가 실제로 어디에 적혀 있는지도 찾았습니다. 라이브러리가 다른 C++ 코드에 자신을 내보낼 때 쓰는 꾸러미(prefab) 안에 abi.json 이라는 작은 파일이 있고, 거기에 "api": 26 처럼 적힙니다. 오늘 다시 열어 보니 넷 다 "api": 24 였는데, 마지막으로 빌드한 앱이 꿈가게라서 그렇습니다.

그러니 이 실패는 빌드 순서에 달려 있습니다. 24인 앱을 빌드한 뒤 26인 앱을 빌드하면 통과하고, 반대 순서면 깨집니다. 깨지는 쪽은 아무것도 안 바꾼 앱이라 원인을 그 앱 안에서 찾으면 영영 못 찾습니다.

지울 것은 네 폴더뿐입니다

고치는 방법은 단순했습니다. reanimated 와 그 짝인 worklets 의 android/build, android/.cxx 네 폴더를 지우고 다시 빌드하면 됩니다. 전부 빌드할 때 다시 만들어지는 결과물이라 지워도 잃는 것이 없습니다.

R=$(ls -d node_modules/.pnpm/react-native-reanimated@*/node_modules/react-native-reanimated/android)
W=$(ls -d node_modules/.pnpm/react-native-worklets@*/node_modules/react-native-worklets/android)
rm -rf "$R/build" "$R/.cxx" "$W/build" "$W/.cxx"

지우고 다시 빌드하자 통과했고, 그 빌드가 그대로 플레이 스토어 심사에 올라갔습니다. 지운 양은 약 2.6GB였습니다. reanimated 의 build 폴더가 1.6GB, .cxx 가 328MB, worklets 쪽이 626MB 와 100MB 였습니다.

node_modules 를 통째로 지우고 다시 설치해도 고쳐지기는 합니다. 다만 그건 몇 분짜리 설치를 다시 하는 일이고, 원인이 어디 있는지 모른 채 넘어가게 됩니다.

같은 함정을 가진 라이브러리가 더 있습니다

이번에 깨진 건 reanimated 였지만, 구조상 같은 일이 날 수 있는 라이브러리를 세어 봤습니다. node_modules/.pnpm 안에서 android/build 폴더를 가진 패키지는 171개였습니다. 그중 버전 숫자를 abi.json 으로 남기는 것은 네 개였습니다.

  • react-native-reanimated
  • react-native-worklets
  • @shopify/react-native-skia
  • react-native-vision-camera

나머지는 빌드 결과물을 남기기는 해도 이 최소 버전 검사에 걸리는 형태가 아니었습니다. 스키아(skia)와 비전 카메라로 실제로 같은 실패가 난 적은 아직 없어서, 거기서도 똑같이 깨지는지는 모릅니다. 다만 둘 다 같은 꾸러미 형식에 같은 숫자를 적고 있으니, 최소 버전이 다른 앱 둘이 이걸 같이 쓰면 같은 일을 겪을 거라고 봅니다.

우리 저장소에 남긴 것

빌드 결과물이 엉뚱한 자리에 남아 있다가 문제를 일으킨 것은 이번이 처음이 아닙니다. 예전에 터보레포(Turborepo) 캐시가 적중했다는데 산출물이 하나도 없었던 일도 같은 부류였습니다.

다음 사람이 같은 메시지를 보고 앱의 최소 버전부터 올리지 않게, 저장소 안내 문서(AGENTS.md)에 증상과 지울 폴더를 적어 두었습니다. 처음부터 막는 방법도 생각해 봤습니다. 모든 앱의 최소 버전을 26으로 맞추면 되지만, 꿈가게 사용자 중 안드로이드 7을 쓰는 사람을 버리는 결정이라 이번에는 하지 않았습니다.

pnpm 이나 그레이들(gradle)에 빌드 결과물을 앱마다 따로 두게 하는 설정이 있는지는 아직 확인하지 않았습니다. npm 이나 yarn 처럼 패키지를 다르게 까는 도구에서도 똑같은지도 재 보지 않았습니다.

2026년 10월 6일 기준, pnpm 워크스페이스와 엑스포 SDK 56, react-native-reanimated 4.3.1 에서 확인했습니다.

이전 글 목표 설정 방법 - 몇 개까지 동시에 가져가도 될까