Skip to main content
입문

재현 가능한 빌드: 같은 소스는 왜 같은 산출물을 보장하지 않는가

소스·환경·빌드 지침과 비교할 산출물의 경계를 정하고, 시간·경로·순서·난수 같은 숨은 입력을 통제한 뒤 독립 재빌드 결과의 바이트를 비교하는 방법을 배운다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

이 글은 네 문장으로 시작한다.

  1. 같은 커밋은 같은 빌드의 입력 하나일 뿐이다.
  2. 먼저 무엇을 같은 결과로 볼지어떤 입력을 고정할지 정한다.
  3. 시간·경로·순서·난수·환경 차이를 통제하고 기존 출력·캐시 없이 다시 만든다.
  4. 마지막에는 실행 성공이 아니라 파일 목록과 모든 바이트가 같은지 비교한다.

기억할 순서는 범위 → 입력 → 변동 → 재빌드 → 비교다.

이 글을 붙잡을 상황은 다음과 같다.

같은 React 커밋을 다시 빌드했는데, 검증한 산출물과 운영에 올릴 산출물의 다이제스트가 다르고 운영 오류의 소스 맵도 맞지 않는다. 무엇이 달라졌는가?

아래의 다섯 단계는 이 차이를 “빌드가 가끔 이상하다”로 남기지 않고 첫 불일치 바이트와 그 값을 만든 숨은 입력까지 좁히는 순서다.

같은 커밋을 다시 빌드한 것이 왜 운영 증거를 깨뜨릴까

대표 상황을 하나 따라가 보자. 지속적 통합(Continuous Integration, CI)에서 검증한 결과와 운영 직전에 다시 만든 결과를 비교한다.

CI 검증 빌드 A
  작업 경로: /workspace/app
  결과: app.js + app.js.map + 산출물 목록 파일 manifest.json
  상태: 테스트와 오류 위치 복원 검증 통과
 
운영 직전 재빌드 B
  작업 경로: /runner/build
  결과: 같은 이름의 app.js + app.js.map + manifest.json
  판단: "같은 커밋이므로 A와 같을 것"

B의 파일 이름과 화면 동작이 같아 보여도, 소스 맵에 작업 경로가 들어가거나 manifest에 현재 시각이 들어가면 바이트와 다이제스트가 달라질 수 있다. 검증한 A의 소스 맵과 실제 배포한 B의 JavaScript를 한 쌍처럼 취급하면 운영 오류 위치가 복원되지 않거나 잘못된 파일을 찾을 수 있다.

잘못된 대응은 같은 커밋이라는 이유만으로 B를 A라고 부르거나, 빌드 명령이 성공했으니 차이를 무시하는 것이다. 이 글의 순서로 바꾸면 다음과 같다.

범위   app.js, app.js.map, manifest.json을 비교 대상으로 고정
입력   커밋·lockfile뿐 아니라 도구·명령·환경·작업 경로를 기록
변동   첫 차이가 절대 경로인지 생성 시각인지 확인
재빌드 숨은 입력을 제거·정규화하고 기존 출력·캐시 없이 다시 생성
비교   파일 목록과 각 다이제스트가 A와 같은지 확인

검증은 “다시 빌드해도 화면이 열린다”에서 끝나지 않는다. 독립 재빌드가 A와 같은 파일 목록과 다이제스트를 만들고, 배포 JavaScript와 정확히 대응하는 소스 맵으로 같은 고정 오류 위치가 복원되는지 확인한다. 재현이 아직 보장되지 않는다면 검증한 A 자체를 다음 환경으로 옮기는 산출물 승격을 11편에서 적용한다.

같은 커밋은 출발점일 뿐이다

여기에는 세 가지 ‘같음’과 한 가지 결과가 있다.

같은 소스 + 같은 환경 + 같은 빌드 지침
                    ↓ 독립 재빌드
            지정한 산출물의 같은 바이트

소스 커밋은 첫 번째 입력만 식별한다. package.json과 해석된 의존성 버전을 기록한 lockfile도 소스에 들어갈 수 있지만, 그 기록만으로 Node.js·Java·Xcode 같은 도구 버전, 운영체제, 환경 변수와 실행 명령까지 고정되지는 않는다. lockfile은 확인된 의존성 관계를 고정하는 중요한 입력이지 전체 빌드 환경의 대체물이 아니다.

‘동일한 기능’, ‘같은 화면’, ‘테스트 통과’도 바이트 동일성과 다르다. 두 JavaScript 파일이 공백이나 생성 시각 한 줄만 달라도 같은 동작을 할 수 있지만 재현된 산출물은 아니다.

먼저 비교할 결과와 환경 경계를 정한다

dist 폴더 전체, JavaScript·스타일 파일·이미지, 소스 맵과 manifest 중 무엇을 비교할지 먼저 적는다. manifest는 산출물의 파일 이름·관계를 적은 목록이다. 로그나 성능 보고서처럼 빌드할 때 함께 생기지만 재현 대상으로 삼지 않을 파일도 구분한다.

로케일(locale)은 언어·지역에 따른 정렬과 표시 규칙이다. 같은 문자열 목록도 로케일이 다르면 대소문자나 문자의 정렬 순서가 바뀔 수 있다. 환경 경계는 무조건 넓히는 것이 아니라 실제로 필요한 입력만 남기도록 줄인다.

비교 범위
  ├─ 포함: app.js, 스타일 파일, app.js.map, manifest.json
  └─ 제외: build.log, 실행 시간 보고서
 
환경 경계
  ├─ Node.js와 패키지 매니저 버전
  ├─ lockfile로 확정한 의존성
  ├─ 빌드 명령·설정·대상·모드
  └─ 결과에 쓰이는 환경 변수와 OS·CPU 전제

서명이 파일 안에 포함되면 서명할 개인 키가 없는 독립 빌드는 같은 서명을 만들 수 없다. 따라서 ‘서명 전 내용’과 ‘최종 서명 패키지’ 중 무엇을 재현 대상으로 삼고 서명을 어떻게 비교할지 선언하지 않은 채 결과가 같거나 다르다고 말하지 않는다.

변동 단계는 다섯 묶음으로 점검한다

핵심 다섯 단계 중 변동을 점검할 때만 다음 하위 목록을 쓴다.

  • 시간 — 배너·manifest·압축 파일의 생성 시각이 바뀐다. 필요 없으면 제거하고, 필요하면 소스에서 정한 시각을 사용한다.
  • 경로 — 소스 맵·디버그 정보에 /Users/... 같은 절대 경로가 남는다. 제거하거나 미리 정한 경로로 정규화한다.
  • 순서 — 파일 목록·manifest 키 순서가 바뀐다. 입력과 출력 키를 명시한 같은 규칙으로 정렬한다.
  • 난수 — 임시 이름·생성 식별자가 바뀐다. 없애거나 소스에서 정한 고정 seed를 사용한다.
  • 환경 — 도구 버전·로케일·시간대·네트워크 응답이 달라진다. 버전·설정·값을 선언하고 외부 입력을 식별한다.

seed(시드)는 의사 난수 생성기가 같은 숫자 순서를 만들게 하는 시작값이다. 보안용 난수까지 무조건 고정하라는 뜻이 아니다. 빌드 결과에 들어가는 무작위 값이 정말 필요한지부터 묻고, 필요하다면 무엇이 그 값을 결정해야 하는지 설계한다.

현재 시각을 소스의 시각으로 바꿀 수 있다

Unix 시각은 1970-01-01 00:00:00 UTC부터 지난 초를 나타내는 정수다. 이 표준은 ‘모든 빌드는 날짜를 지워야 한다’고 요구하지 않는다. 같은 소스에서 같은 시각이 결정되게 한다.

나쁜 입력: 빌드를 실행한 현재 시각
좋은 입력: 소스 커밋 시각이나 저장소 파일에 기록한 릴리스 시각에서 정한 SOURCE_DATE_EPOCH
확인:      실제 도구가 값을 읽었는지 산출물로 비교

정렬했다고 끝나지 않는다

파일 시스템은 디렉터리 목록 순서를 항상 보장하지 않는다. 목록을 정렬해도 로케일에 따라 정렬 결과가 다를 수 있다. 따라서 ‘정렬한다’와 ‘어떤 정렬 규칙을 사용한다’를 함께 고정한다. 객체나 집합을 순회해 manifest를 만들 때도 출력 전에 키 순서를 안정화한다.

격리는 재현성을 돕지만 대신하지 않는다

컨테이너는 운영체제와 도구를 나눠 주는 유용한 수단이지 그 자체가 격리 빌드는 아니다. latest처럼 고정된 이미지 버전을 가리키지 않는 컨테이너 태그, 실행 CPU에 따른 최적화, 외부 네트워크에서 내려받는 최신 파일, 현재 시각과 난수까지 자동으로 고정하지는 않는다.

격리 빌드가 묻는 질문: 선언한 입력만 관찰해 같은 입력에 같은 결과를 내는가?
재현 가능한 빌드가 묻는 질문: 다시 만든 지정 산출물의 바이트가 같은가?

두 목표는 서로 돕지만 하나를 달성했다고 다른 하나를 추정하지 않는다.

다섯 단계로 확인한다

실무 절차도 기억한 다섯 단어를 그대로 따른다.

  1. 범위 — 재현돼야 할 파일과 제외할 부가 결과를 적는다.
  2. 입력 — 소스 리비전, lockfile, 도구·OS·아키텍처, 명령, 설정과 환경 값을 기록한다.
  3. 변동 — 시간·경로·순서·난수·네트워크처럼 매 실행 달라질 값을 제거하거나 고정한다.
  4. 재빌드 — 기존 출력과 캐시를 재사용하지 않는 별도 디렉터리나 환경에서 다시 만든다.
  5. 비교 — 파일 목록, 크기와 다이제스트를 비교하고 첫 불일치를 조사한다.

‘독립 재빌드’는 반드시 다른 회사나 다른 운영체제에서 한다는 뜻은 아니다. 이전 출력과 캐시를 그대로 재사용하지 않고 선언한 입력만으로 새 결과를 만든다는 뜻이다. 이는 바깥 입력을 차단하는 격리 빌드와도 다른 조건이다. 가능하면 시간과 작업 경로를 바꾸거나 별도 빌드 실행자를 사용해 숨은 입력을 드러낸다.

예를 들어 manifest.json 한 파일만 다르다면 다음처럼 좁힌다.

파일 목록은 같은가?
  ├─ 아니오 → 그래프·조건·환경 입력 확인
  └─ 예

첫 다른 값은 생성 시각인가? 경로인가? 배열·키 순서인가? 식별자인가?

그 값을 만든 입력을 선언·제거·정규화한 뒤 다시 두 번 빌드

캐시를 끄는 것은 첫 확인에 유용하지만 최종 운영에서 캐시를 영원히 금지하라는 뜻은 아니다. 재현 가능한 결과가 기준선이 된 뒤, 다음 글에서 어떤 입력이 같을 때 캐시를 안전하게 재사용할지 다룬다.

Web·React·React Native에서는 산출물 경계를 먼저 나눈다

Web과 React

React를 사용하는 Web 프로젝트에서는 빌드 도구가 설정에 따라 컴포넌트 코드를 여러 JavaScript 청크와 스타일·자산으로 만들 수 있다. 내용 기반 파일 이름도 결과 바이트가 달라지면 함께 달라질 수 있다. 파일 이름만 같다고 내용을 같다고 보지 않고, 파일 목록과 각 다이제스트를 비교한다.

소스 맵도 대상에 포함한다면 배포 코드와 정확한 한 쌍이어야 한다. 작업 디렉터리 절대 경로가 맵에 들어가면 다른 폴더에서 만든 결과가 달라질 수 있으므로 경로 표기와 원본 소스 본문을 맵에 넣는 sourcesContent 정책도 입력 계약에 포함한다.

React Native

React Native에는 JavaScript 번들 또는 Hermes가 JavaScript를 변환해 만든 실행 형식과 그 소스 맵, iOS·Android 네이티브 빌드 산출물이 함께 존재할 수 있다. JavaScript 계층이 재현됐다고 앱 패키지 전체까지 자동으로 재현됐다고 말하지 않는다. 반대로 네이티브 서명 단계의 차이가 있다고 JavaScript 번들 비교까지 포기할 필요도 없다.

JavaScript 경계: 번들·Hermes 결과 + 정확한 소스 맵
네이티브 경계:   플랫폼 도구 산출물 + 자산 + 패키징 메타데이터
서명 경계:       서명 전 내용 / 최종 서명 패키지 중 비교 대상을 명시

플랫폼 도구와 서명 규칙은 버전에 묶인 별도 주제다. 이 글에서는 ‘같다’고 말하는 단위를 나누고 각 단위의 입력을 기록한다는 원리까지만 사용한다.

출처 기록과 바이트 동일성은 다른 증거다

증거의 질문을 세 묶음으로 나누면 혼동이 줄어든다.

  • 콘텐츠 다이제스트 → “이 두 파일의 선택한 다이제스트가 같은가?”에 답한다. 충돌 가능성이 이론적으로 남는 실용적 대리 비교이며, 누가 어떤 입력으로 만들었는지도 알려 주지 않는다.
  • 빌드 출처 증명 → “어떤 빌더가 어떤 입력·과정으로 만들었다고 기록했는가?”에 답한다. 독립 재빌드 결과가 같다는 사실은 알려 주지 않는다.
  • 독립 재빌드 비교 → “선언한 입력으로 같은 결과를 다시 만들었는가?”에 답한다. 기록과 빌더를 신뢰해도 되는지는 별도 검증이 필요하다.

출처 증명에 다이제스트가 들어가도 역할은 구분한다. 그 값은 기록이 가리키는 산출물을 식별한다. 다른 빌드가 같은 결과를 냈는지는 두 결과를 실제로 비교해야 한다.

오판도 다섯 단계로 고친다

  • 범위 — 해시 모양의 파일 이름 하나가 아니라 지정한 파일 목록 전체를 정한다.
  • 입력 — 같은 커밋과 lockfile에 더해 도구·OS·환경 값과 빌드 지침을 기록한다.
  • 변동 — 컨테이너를 사용해도 시간·경로·순서·난수와 외부 입력을 다시 확인한다.
  • 재빌드 — 출처 기록만 믿지 않고 이전 출력·캐시 없이 새 결과를 만든다.
  • 비교 — 명령 성공이나 같은 파일 이름이 아니라 실제 다이제스트를 비교한다.

이 글의 경계

이번 글은 같은 입력에서 같은 산출물을 다시 만들고 확인하는 계약에 집중했다. 다음 내용은 분리한다.

  • 이전 결과를 언제 안전하게 재사용하는가: 다음 8편 빌드 캐시
  • 재현한 산출물을 어느 검증 단계에서 만드는가: 10편 CI와 CD
  • 테스트한 것과 배포한 것이 같은 산출물인지 증명하는가: 11편 동일성과 승격
  • 서명과 출처 증명의 전체 공급망 신뢰 모델: 별도 공급망 보안 글
  • iOS·Android 앱 패키지와 서명의 버전별 재현 조건: 플랫폼 전용 글

마지막으로 한 줄만 기억한다. 범위를 정하고 입력을 고정한 뒤 독립적으로 다시 만들어 바이트를 비교한다.

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01재현 가능한 빌드라고 말하려면 무엇이 같아야 할까?

    정답

    같은 소스·빌드 환경·빌드 지침으로 독립적으로 다시 만들었을 때 미리 지정한 산출물의 모든 바이트가 같아야 한다.

    왜 그런가

    화면과 테스트 결과가 같아 보이는 것보다 강한 조건이며, 비교할 산출물과 입력 경계를 먼저 선언해야 한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02같은 커밋인데 산출물 다이제스트가 달라졌다면 무엇을 먼저 의심할까?

    정답

    도구·의존성 버전과 설정뿐 아니라 현재 시각, 경로, 파일 순서, 로케일·시간대, 난수와 선언하지 않은 네트워크 입력을 확인한다.

    왜 그런가

    소스 저장소 밖의 값도 빌드가 읽어 산출물에 기록하거나 처리 순서를 바꾸면 실제 입력이다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03컨테이너 안에서 빌드하면 재현 가능한 빌드가 자동으로 보장될까?

    정답

    아니다. 호스트 격리에는 도움이 되지만 고정하지 않은 컨테이너 이미지 버전이나 CPU 아키텍처, 시간·난수처럼 여전히 변하는 입력이 있을 수 있어 독립 결과를 비교해야 한다.

    왜 그런가

    컨테이너 격리는 입력 경계를 줄이는 수단일 뿐이다. 격리 빌드는 선언한 입력이 같으면 언제나 같은 결과를 내야 하고, 재현 가능성 주장은 독립 산출물 비교로 확인한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04빌드 출처 증명이 있으면 산출물이 재현됐다고 볼 수 있을까?

    정답

    아니다. 출처 증명은 어디서 어떤 입력과 과정으로 만들었는지 추적하게 하고, 재현 여부는 별도 빌드 결과의 바이트나 다이제스트를 비교해 확인한다.

    왜 그런가

    기록의 책임과 결과 동일성의 책임을 분리해야 어느 증거가 빠졌는지 판단할 수 있다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  5. 05재현 가능성을 확인하는 가장 짧은 순서는 무엇일까?

    정답

    비교 범위를 정하고, 입력을 기록·고정하고, 변동값을 제거한 뒤, 기존 출력·캐시를 재사용하지 않고 독립 재빌드하고, 파일 목록과 다이제스트를 비교한다.

    왜 그런가

    차이가 나면 첫 불일치 파일과 바이트에서 역으로 시간·경로·순서·난수·환경 입력을 좁힌다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Definitions — When is a build reproducible?Reproducible Builds · 공식 문서 · 확인 2026-08-04
  2. What's in a build environment?Reproducible Builds · 공식 문서 · 확인 2026-08-04
  3. Stable order for inputsReproducible Builds · 공식 문서 · 확인 2026-08-04
  4. Stable order for outputsReproducible Builds · 공식 문서 · 확인 2026-08-04
  5. RandomnessReproducible Builds · 공식 문서 · 확인 2026-08-04
  6. Build pathReproducible Builds · 공식 문서 · 확인 2026-08-04
  7. SOURCE_DATE_EPOCH specificationReproducible Builds · 표준 · 확인 2026-08-04
  8. Cryptographic checksumsReproducible Builds · 공식 문서 · 확인 2026-08-04
  9. Embedded signaturesReproducible Builds · 공식 문서 · 확인 2026-08-04
  10. HermeticityBazel · 공식 문서 · 확인 2026-08-04
  11. Building for ProductionVite · 공식 문서 · 확인 2026-08-04
  12. Debugging Release BuildsReact Native · 공식 문서 · 확인 2026-08-04
  13. Publishing to Google Play StoreReact Native · 공식 문서 · 확인 2026-08-04
  14. React Native Gradle PluginReact Native · 공식 문서 · 확인 2026-08-04
  15. SLSA v1.2 — ProvenanceSLSA · 표준 · 확인 2026-08-04
  16. SLSA v1.2 — Build ProvenanceSLSA · 표준 · 확인 2026-08-04
이 문서의 마지막까지 읽었습니다.