배포

어제 되던 설치가 오늘 깨진 이유

같은 소스인데 설치 결과가 달라질 수 있어요. package.json과 lockfile이 기록하는 것, 배포에서 차이를 발견하면 멈추는 이유를 알아봐요.

이 글의 목차package.json은 목록이고 lockfile은 영수증이다5
  1. package.json은 목록이고 lockfile은 영수증이다
  2. npm이 영수증을 기본으로 남기기까지
  3. 고쳐 주는 설치와 멈추는 설치
  4. lockfile이 얼리지 못하는 것
  5. 제가 버리던 영수증을 다시 본 날

장보기 목록에는 우유와 빵이라고만 적혀 있고, 영수증에는 어느 회사의 어떤 제품을 얼마에 샀는지가 남아요. 목록만 들고 다시 가면 약속을 어기지 않고도 식탁 위 결과가 달라져요.

Node.js 프로젝트의 설치가 그래요. 소스 코드는 한 줄도 바뀌지 않았는데 오늘 새로 만든 서버만 시작하지 못하는 일이 생겨요. 원인은 코드가 아니라 그 아래에서 설치 시점마다 다시 계산되는 조합에 있고, 이 조합을 기록으로 고정하느냐 아니냐가 배포의 재현성을 갈라요.

설치 시점에 따른 의존성 트리 선택 package.json의 허용 범위만 쓰면 설치 시점마다 직접·간접 의존성을 다시 고르지만, lockfile은 선택한 트리를 기록한다. 개념 도해 설치 시점에 따른 의존성 트리 선택 버전 선택 경로 ① package.json 범위 ② 설치 시점의 후보 시점 A 시점 B ③ 직접 버전 A ③ 직접 버전 B ④ 간접 의존성 트리 A ④ 간접 의존성 트리 B 기록 유무별 트리 선택 범위 기록된 트리 버전 범위만 설치 A 설치 B 트리 차이 lockfile 포함 같은 트리 선택 가능한 의존성 트리 package.json의 허용 범위만 쓰면 설치 시점마다 직접·간접 의존성을 다시 고르지만, lockfile은 선택한 트리를 기록한다. SOSHIN · DEV NOTES
허용 범위만 남기면 다음 설치가 새로운 의존성 조합을 고를 수 있어요.

package.json은 목록이고 lockfile은 영수증이다

package.json에는 애플리케이션이 직접 사용하는 패키지와 허용할 버전 범위가 적혀요. 범위 안에서 더 새로운 버전을 고를 수 있으므로 개발자는 작은 수정이 나올 때마다 목록을 다시 쓰지 않아도 돼요.

그 패키지들도 저마다 다른 패키지를 필요로 해요. 직접 고른 부품 아래에 간접 부품이 가지처럼 이어지는 모습을 의존성 트리(dependency tree)라고 불러요. 화면에는 패키지 하나를 추가한 것처럼 보여도 실제 설치에는 훨씬 많은 선택이 따라와요.

package-lock.json은 npm이 그 순간 고른 정확한 버전과 다운로드 위치, 파일 무결성 정보를 트리 전체에 기록해요. npm 공식 문서도 lockfile을 저장소에 함께 보관해 팀원과 배포 환경이 같은 트리를 설치하도록 사용하라고 설명해요. 목록이 “무엇이 필요한가”를 말한다면 lockfile은 “그때 정확히 무엇을 골랐는가”를 남기는 영수증이에요.

npm이 영수증을 기본으로 남기기까지

초기의 npm에도 설치 결과를 고정하는 장치는 있었지만 별도로 선택해 사용하는 성격이 강했어요. 자바스크립트 생태계가 커지고 한 프로젝트가 끌어오는 패키지가 많아지자, 개발자마다 다른 버전을 설치하는 드리프트(drift)가 자주 문제를 만들었어요. 드리프트는 같은 출발점이 시간이 지나며 조금씩 다른 상태로 벌어지는 현상이에요.

npm 팀은 2017년 5월 npm 5를 공개하면서 package-lock을 기본 동작으로 만들었어요. 설치 결과를 우연한 로컬 상태가 아니라 프로젝트 기록의 일부로 남기려는 변화였어요.

이듬해 npm 5.7에는 npm ci가 추가됐어요. 이름의 ci는 지속적 통합(Continuous Integration)을 가리켜요. 개발자가 요구사항을 바꾸는 자리와 이미 검토한 결과를 재현하는 자리는 목적이 다르다는 점을 명령의 동작으로 나눈 것이에요.

고쳐 주는 설치와 멈추는 설치

개발 중에는 패키지를 추가하거나 버전 범위를 바꾸면서 lockfile도 새 요구사항에 맞게 갱신해야 해요. 반면 배포에서는 두 파일이 맞지 않을 때 자동으로 새로운 답을 만드는 것보다, 예상하지 않은 차이를 발견하고 멈추는 편이 안전해요.

npm ci는 기존 lockfile을 필요로 하고 package.json과 내용이 다르면 파일을 고쳐 쓰지 않은 채 실패해요. 기존 설치 폴더도 비운 뒤 기록된 트리를 다시 만들어요. 현재 npm 문서가 이를 사실상 동결된 설치라고 표현하는 이유예요.

상황개발 중 설치CI와 배포의 설치
목적새 요구사항을 계산하고 기록한다검토된 트리를 재현한다
두 파일이 다를 때lockfile을 갱신할 수 있다파일을 바꾸지 않고 실패한다
실패의 의미작업을 이어 가기 위한 수정이 필요하다예상하지 않은 배포 변경을 막았다는 신호다

두 파일의 차이 때문에 배포가 멈췄다면, 먼저 개발하면서 의존성을 바꾼 의도가 있었는지 보면 돼요. 의도한 변경이면 설치 기록까지 함께 검토하고, 의도하지 않았다면 어디서 차이가 생겼는지 찾는 거예요. 배포 단계가 기록을 자동으로 고쳐 버리면 그 판단을 건너뛰게 돼요.

덧붙이면 lockfile에는 각 패키지의 무결성 값이 함께 적혀요. 내려받은 파일이 기록된 지문과 다르면 설치가 실패하므로, lockfile은 버전을 고정하는 장치이면서 내려받은 내용이 그때 그것인지 확인하는 장치이기도 해요. 브라우저가 외부 스크립트에 하는 검사와 원리가 같아요.

맞지 않는 기록을 조용히 고치지 않아요

요구 버전만 바꾼 상태로 CI 설치를 시도한 결과를 보세요.

요구사항

1.x

설치 기록

1.0.0

요구사항과 일치

npm ci는 요구사항과 lockfile이 다르면 기록을 고치는 대신 실패해요. 두 파일을 함께 검토해 맞춰야 해요.

예시 패키지 하나의 버전 비교예요. 실제 설치를 실행하지 않으며 OS·CPU·빌드 도구까지 같은 결과를 보장하지 않아요.

lockfile이 얼리지 못하는 것

lockfile은 npm이 고른 의존성 트리를 기록하지만 빌드 세계 전체를 얼리지는 못해요. 운영체제와 CPU 구조, Node.js와 npm 자체의 버전, 컨테이너 바탕 이미지, 네이티브 모듈의 컴파일 결과는 여전히 달라질 수 있어요. 설치 과정이 외부에서 파일을 새로 받는다면 그 경로도 별개의 변수예요.

그래서 lockfile은 완전한 재현성의 종착점이 아니라 가장 먼저 닫아야 할 경계예요. 직접 작성한 몇 줄뿐 아니라 그 아래에서 함께 실행되는 코드의 선택도 리뷰와 변경 기록 안으로 가져와요.

조치 시점에 따른 복구와 재발 방지 검증한 조합으로 되돌리면 현재 장애가 끝나고, lockfile과 npm ci는 다음 설치 차이를 운영 반영 전에 멈춘다. 개념 도해 조치 시점에 따른 복구와 재발 방지 장애 복구 경로 ① 새 트리 선택 ② 실행 실패 ③ 검증한 조합 복원 ④ 서비스 정상 다음 배포의 운영 반영 경계 운영 반영선 파일 일치 ① npm ci 검사 ② 기록 트리 설치 ③ 운영 반영 파일 불일치 ① npm ci 검사 ② 빌드 중단 ③ 운영 미반영 배포 진행 검증한 조합으로 되돌리면 현재 장애가 끝나고, lockfile과 npm ci는 다음 설치 차이를 운영 반영 전에 멈춘다. SOSHIN · DEV NOTES
문제 버전을 되돌리는 일은 복구이고, 설치 차이를 빌드에서 멈추게 하는 일은 재발 방지예요.

제가 버리던 영수증을 다시 본 날

제가 이 차이를 선명하게 배운 것은 소스가 그대로인데 새 배포만 반복해서 시작하지 못한 날이었어요. 빌드 과정이 lockfile을 사용하지 않아 설치 시점의 새로운 의존성 조합을 골랐고, 그중 한 부품이 실행 환경과 맞지 않았어요. 우선 문제가 생기기 전 조합으로 돌아가 서비스를 복구했어요.

그 뒤에는 정확한 설치 기록을 저장소에 남기고 배포가 그 기록과 다르면 멈추도록 바꿨어요. 복구와 재발 방지가 다른 작업이라는 것도 그때 갈렸어요. 문제 조합에서 벗어나는 일은 복구이고, 다음 배포가 같은 방식으로 달라지지 못하게 막는 일은 별도의 조치였어요.

지금은 리뷰에서 lockfile 변경을 코드 변경과 같은 무게로 봐요. 그 파일에 적힌 것이 실제로 운영에서 실행될 코드의 목록이고, 대부분의 줄은 제가 직접 고른 적이 없는 패키지예요. 제가 쓴 몇 줄보다 그 아래가 훨씬 넓다는 사실이 그날 이후로는 추상적인 경고가 아니게 됐어요.

소신을 상징하는 까마귀 목판화 일러스트
작성자소신 · Soshin

웹과 서버를 만들고, 개발하며 배운 것들을 글로 남깁니다.

좌우로 움직여 보세요