Node.js 업그레이드 뒤 tree-sitter 빌드가 V8 헤더의 requires 오류로 실패하는 이유

최신 Node.js로 올린 개발 장비에서 코드 그래프 도구를 전역 설치하다가, 의존성인 tree-sitter의 네이티브 빌드가 V8 헤더 안에서 낯선 문법 오류로 멈췄다. 우리 코드도 아니고 V8 헤더에서 터진 오류라 처음엔 컴파일러 고장처럼 보였다. 원인은 C++ 표준 버전 불일치였고 해결은 한 줄이었다. tree-sitter처럼 node-gyp로 빌드하는 패키지를 쓰는 사람에게 도움이 되도록 정리한다.
상황
macOS 개발 장비의 Node.js를 26으로 올린 뒤, 소스 코드를 그래프로 색인하는 CLI 도구를 `npm install -g`로 새 버전 설치했다. 이 도구는 파서로 tree-sitter를 쓰고, 설치 과정에서 tree-sitter 0.21.1의 네이티브 애드온을 node-gyp로 컴파일한다. 같은 도구는 예전 Node.js 버전에서는 문제없이 설치됐다.
증상
- `npm install -g` 단계에서 node-gyp 빌드가 실패하고 설치가 중단된다.
- 오류 위치가 tree-sitter 소스가 아니라 Node.js가 내려받은 V8 헤더(`v8-internal.h`)다.
- 메시지는 `unknown type name 'requires'` 같은 문법 오류다. `requires`를 타입 이름으로 읽었다는 뜻이다.
- 설치가 중간에 끊기면서 전역 bin 폴더에 깨진 심볼릭 링크가 남을 수 있다.
원인
`requires`는 C++20에서 들어온 키워드다. 콘셉트(concept)와 함께 템플릿 제약을 적을 때 쓴다. 최근 Node.js가 묶어 내는 V8 헤더는 이런 C++20 문법을 직접 쓴다. 따라서 이 헤더를 포함하는 네이티브 애드온은 C++20 이상으로 컴파일해야 한다.
그런데 tree-sitter 0.21.x의 `binding.gyp`는 C++17로 컴파일하도록 고정돼 있었다. 컴파일러는 C++17 규칙으로 V8 헤더를 읽다가 `requires`를 모르는 단어로 보고 실패한다. 즉 컴파일러 문제가 아니라 "애드온이 지정한 언어 표준"과 "Node.js 헤더가 요구하는 언어 표준"이 어긋난 것이다.
참고로 tree-sitter의 Node 바인딩 저장소 최신 `binding.gyp`는 빌드 대상 Node.js 버전이 22 미만이면 C++17, 22 이상이면 C++20을 고르도록 바뀌어 있다. 상위 프로젝트에서도 같은 문제를 겪고 고친 흔적이다. 문제는 우리가 쓰려던 도구가 아직 옛 tree-sitter 버전에 묶여 있었다는 점이다.
해결
설치 명령에 C++ 컴파일 플래그를 환경 변수로 넘겨 C++20을 강제했다.
```bash
CXXFLAGS="-std=c++20" npm install -g some-graph-cli
```
node-gyp가 만드는 빌드 설정은 환경의 `CXXFLAGS`를 함께 쓰므로, 뒤에 붙은 `-std=c++20`이 바인딩의 C++17 지정보다 우선 적용된다. 이렇게 하면 V8 헤더가 정상 해석되고 설치가 끝난다.
여러 장비에 같은 도구를 깔아 주는 설치 스크립트가 있었기 때문에, 이 값을 도구별 빌드 환경 설정에 고정하고 테스트에서 실제로 `CXXFLAGS`가 전달되는지 확인하도록 했다. 사람이 기억해서 붙이는 방식은 다음 장비에서 반드시 빠진다.
운영자 해설
이 오류를 처음 보면 Xcode 명령줄 도구를 다시 깔거나 node-gyp 캐시를 지우는 쪽으로 손이 가기 쉽다. 하지만 메시지에 C++20 키워드(`requires`, `concept`)가 보이고 위치가 V8 헤더라면, 거의 항상 언어 표준 불일치다. 이 패턴을 알아 두면 진단 시간이 크게 줄어든다.
근본 해결은 의존성을 C++ 표준을 Node.js 버전에 맞춰 고르는 tree-sitter 버전으로 올리는 것이다. `CXXFLAGS`는 도구 쪽이 업데이트되기 전까지 쓰는 우회책으로 보는 편이 맞다. 우회책을 넣었다면 언제 뺄 수 있는지 조건(상위 도구가 새 tree-sitter를 쓰게 되는 시점)도 함께 적어 두자.
또 하나, Node.js 메이저 버전을 올리는 순간 전역 설치한 네이티브 도구는 모두 재빌드 대상이다. 업그레이드 직후 전역 도구를 한 번씩 다시 설치해 보는 점검을 루틴에 넣어 두면 실제 작업 중에 막히는 일을 줄일 수 있다.
다시 겪지 않으려면
- node-gyp 오류가 V8 헤더에서 나고 `requires`·`concept`가 보이면 C++ 표준부터 의심한다.
- 의존성의 `binding.gyp`가 C++ 표준을 어떻게 정하는지 확인한다.
- 당장은 `CXXFLAGS="-std=c++20"`로 우회하고, 설치 스크립트에 고정해 테스트로 확인한다.
- 근본적으로는 Node.js 버전에 맞춰 표준을 고르는 새 tree-sitter로 올린다.
- Node.js 메이저 업그레이드 뒤에는 전역 네이티브 도구 재설치를 점검 목록에 넣는다.
- 설치가 실패했다면 전역 bin 폴더에 남은 깨진 링크를 정리한다.
참고 자료
- node-tree-sitter binding.gyp — tree-sitter (GitHub)
- node-gyp: Node.js native addon build tool — Node.js (GitHub)