[2026] Git submodule 서브모듈 실무 | 추가·업데이트·CI·모노레포 대안

[2026] Git submodule 서브모듈 실무 | 추가·업데이트·CI·모노레포 대안

이 글의 핵심

Git submodule로 서브레포를 끌어오는 법, 초기화·업데이트·삭제, CI 캐시·흔한 오류, submodule 대신 모노레포를 쓰는 기준까지 정리합니다.

들어가며

Git submodule은 한 저장소 안에 다른 Git 저장소를 디렉터리로 포함시키는 방식입니다. 상위 저장소(슈퍼프로젝트)는 하위 저장소의 특정 커밋 SHA만 기록하므로, “의존 라이브러리를 소스로 포함하되 버전을 엄격히 핀한다”는 요구에 맞습니다. 반면 클론 한 번에 모든 것이 끝나지 않고, CI·Docker 빌드에서 초기화 단계를 빼먹기 쉬워 운영 난이도가 올라갑니다. 이 글은 Git submodule 서브모듈 실무 키워드에 맞춰 추가·업데이트·삭제, CI 설정, 흔한 오류, 모노레포 대안까지 정리합니다.

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

목차

  1. 개념: 슈퍼프로젝트와 gitlink
  2. 실전: 추가·클론·업데이트·삭제
  3. 고급: shallow·포크·대체 URL
  4. 비교: submodule vs subtree vs 패키지 vs 모노레포
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

  • 상위 저장소의 .gitmodules경로·URL·브랜치 추적 설정이 기록됩니다.
  • Git은 하위 디렉터리를 일반 파일이 아니라 gitlink(커밋 포인터)로 취급합니다.
  • 따라서 “하위 저장소의 최신 main을 자동으로 따라간다”가 기본은 아닙니다. 상위에 기록된 SHA가 진실입니다.

실전: 추가·클론·업데이트·삭제

서브모듈 추가

기본 추가: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 서브모듈 추가
git submodule add https://github.com/org/shared-lib.git vendor/shared-lib
# 상태 확인
git status
# Changes to be committed:
#   new file:   .gitmodules
#   new file:   vendor/shared-lib (커밋 SHA)
# 커밋
git commit -m "chore: add shared-lib submodule"
git push

특정 브랜치 추적: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# develop 브랜치 추적
git submodule add -b develop https://github.com/org/shared-lib.git vendor/shared-lib
# .gitmodules 확인
cat .gitmodules
# [submodule "vendor/shared-lib"]
#     path = vendor/shared-lib
#     url = https://github.com/org/shared-lib.git
#     branch = develop

.gitmodules 파일 구조: 아래 코드는 ini를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

[submodule "vendor/shared-lib"]
    path = vendor/shared-lib
    url = https://github.com/org/shared-lib.git
    branch = main
    
[submodule "vendor/utils"]
    path = vendor/utils
    url = https://github.com/org/utils.git
    branch = stable

처음 클론할 때

방법 1: 클론 시 서브모듈 포함 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 모든 서브모듈 함께 클론
git clone --recurse-submodules https://github.com/org/main-app.git
# 또는 (동일)
git clone --recursive https://github.com/org/main-app.git

방법 2: 클론 후 서브모듈 초기화 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 먼저 메인 저장소 클론
git clone https://github.com/org/main-app.git
cd main-app
# 서브모듈 초기화 및 가져오기
git submodule init
git submodule update
# 또는 한 번에
git submodule update --init --recursive

방법 3: 병렬 클론 (빠름)

# 서브모듈을 병렬로 클론
git clone --recurse-submodules --jobs 4 https://github.com/org/main-app.git

서브모듈 상태 확인

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 서브모듈 목록 및 상태
git submodule status
#  abc1234 vendor/shared-lib (v1.2.3)
# +def5678 vendor/utils (v2.0.0-5-gdef5678)
# -ghi9012 vendor/legacy (heads/main)
# 기호 의미:
# (공백): 정상 (상위가 가리키는 커밋과 일치)
# +: 서브모듈이 다른 커밋을 체크아웃 (변경됨)
# -: 서브모듈이 초기화되지 않음
# U: 병합 충돌
# 상세 정보
git submodule foreach 'echo $name: $(git rev-parse HEAD)'

하위 저장소를 최신으로 올리기

방법 1: 수동 업데이트 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 서브모듈 디렉터리로 이동
cd vendor/shared-lib
# 최신 코드 가져오기
git fetch origin
git checkout main
git pull origin main
# 상위 저장소로 돌아와서 변경 기록
cd ../..
git add vendor/shared-lib
git commit -m "chore: update shared-lib to v1.3.0"
git push

방법 2: 자동 업데이트 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 모든 서브모듈을 원격 브랜치 최신으로
git submodule update --remote
# 특정 서브모듈만
git submodule update --remote vendor/shared-lib
# 변경사항 커밋
git add .gitmodules vendor/shared-lib
git commit -m "chore: update submodules to latest"

방법 3: 특정 태그로 고정 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

cd vendor/shared-lib
git fetch --tags
git checkout v1.3.0
cd ../..
git add vendor/shared-lib
git commit -m "chore: pin shared-lib to v1.3.0"

팀원이 변경사항 받기

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 상위 저장소 업데이트
git pull
# 서브모듈도 업데이트
git submodule update --init --recursive
# 또는 한 번에
git pull --recurse-submodules

자동화 (Git 설정): 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# pull 시 항상 서브모듈 업데이트
git config submodule.recurse true
# 이제 git pull만 해도 서브모듈 자동 업데이트
git pull

서브모듈 제거

완전 제거 절차: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 1. 서브모듈 등록 해제
git submodule deinit -f vendor/shared-lib
# 2. Git에서 제거
git rm -f vendor/shared-lib
# 3. .git/modules 정리 (선택)
rm -rf .git/modules/vendor/shared-lib
# 4. 커밋
git commit -m "chore: remove shared-lib submodule"
git push

부분 제거 (나중에 다시 추가 가능): 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 등록만 해제 (디렉터리는 유지)
git submodule deinit vendor/shared-lib
# 다시 활성화
git submodule update --init vendor/shared-lib


고급: shallow·포크·대체 URL

CI에서 빠른 fetch

얕은 클론으로 시간 단축: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 히스토리 1개만 가져오기
git submodule update --init --recursive --depth 1
# 또는 클론 시
git clone --recurse-submodules --shallow-submodules https://github.com/org/main-app.git

GitHub Actions 예제: 다음은 yaml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout with submodules
        uses: actions/checkout@v4
        with:
          submodules: recursive
          
      - name: Build
        run: |
          make build

GitLab CI 예제: 아래 코드는 yaml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

variables:
  GIT_SUBMODULE_STRATEGY: recursive
build:
  script:
    - git submodule sync
    - git submodule update --init --recursive --depth 1
    - make build

Docker 빌드: 다음은 dockerfile를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

FROM node:18
WORKDIR /app
# Git 설치 (서브모듈 클론에 필요)
RUN apt-get update && apt-get install -y git
# 저장소 클론
COPY . .
# 서브모듈 초기화
RUN git submodule update --init --recursive --depth 1
# 빌드
RUN npm install && npm run build
CMD ["npm", "start"]

URL 재매핑 (엔터프라이즈 미러)

로컬 설정: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 특정 서브모듈 URL 변경
git config submodule.vendor/shared-lib.url https://git.internal.corp/org/shared-lib.git
# 모든 GitHub URL을 내부 미러로
git config --global url."https://git.internal.corp/".insteadOf "https://github.com/"

SSH ↔ HTTPS 전환: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# HTTPS를 SSH로
git config --global url."git@github.com:".insteadOf "https://github.com/"
# SSH를 HTTPS로 (CI에서 유용)
git config --global url."https://github.com/".insteadOf "git@github.com:"

서브모듈에서 작업하기

브랜치 생성 및 푸시: 다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 서브모듈로 이동
cd vendor/shared-lib
# 현재 상태 확인 (detached HEAD일 가능성 높음)
git status
# HEAD detached at abc1234
# 브랜치 생성
git checkout -b feature/my-change
# 작업 후 커밋
git add .
git commit -m "feat: add new feature"
# 푸시 (upstream 설정)
git push -u origin feature/my-change
# PR 생성 후 머지되면 상위 저장소 업데이트
cd ../..
git submodule update --remote vendor/shared-lib
git add vendor/shared-lib
git commit -m "chore: update shared-lib with new feature"

서브모듈 포크 사용

원본 대신 포크 사용: 다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 1. 원본 서브모듈 제거
git submodule deinit -f vendor/shared-lib
git rm -f vendor/shared-lib
# 2. 포크 추가
git submodule add https://github.com/myorg/shared-lib.git vendor/shared-lib
# 3. 원본을 upstream으로 추가
cd vendor/shared-lib
git remote add upstream https://github.com/org/shared-lib.git
git fetch upstream
# 4. 원본 변경사항 가져오기
git merge upstream/main
git push origin main
cd ../..
git add vendor/shared-lib
git commit -m "chore: switch to forked shared-lib"


비교: submodule vs subtree vs 패키지 vs 모노레포

상세 비교표

항목SubmoduleSubtree패키지 (npm/pip)모노레포
클론 복잡도추가 명령 필요단순 (git clone)단순 (패키지 설치)단순
버전 관리커밋 SHA커밋 히스토리시맨틱 버전단일 버전
업데이트수동 (명시적)수동 (git subtree pull)자동 (패키지 매니저)즉시 반영
독립 개발✅ 쉬움⚠️ 복잡✅ 쉬움❌ 어려움
권한 관리레포별 분리레포별 분리레지스트리 권한단일 레포
CI 복잡도높음중간낮음낮음
학습 곡선가파름가파름낮음중간

Submodule 예제

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 구조
main-app/
├── src/
├── vendor/
   ├── shared-lib/  (서브모듈)
   └── utils/       (서브모듈)
└── .gitmodules
# 클론
git clone --recurse-submodules https://github.com/org/main-app.git
# 업데이트
cd vendor/shared-lib
git pull origin main
cd ../..
git add vendor/shared-lib
git commit -m "update"

Subtree 예제

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 추가
git subtree add --prefix=vendor/shared-lib https://github.com/org/shared-lib.git main --squash
# 구조 (일반 디렉터리처럼 보임)
main-app/
├── src/
└── vendor/
    └── shared-lib/  (일반 디렉터리, 히스토리 포함)
# 업데이트
git subtree pull --prefix=vendor/shared-lib https://github.com/org/shared-lib.git main --squash
# 변경사항 업스트림에 푸시
git subtree push --prefix=vendor/shared-lib https://github.com/org/shared-lib.git feature-branch

패키지 예제 (npm)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 구조
main-app/
├── src/
├── node_modules/
   └── @org/shared-lib/  (npm 패키지)
└── package.json
# 설치
npm install @org/shared-lib
# 업데이트
npm update @org/shared-lib
# package.json
{
  "dependencies": {
    "@org/shared-lib": "^1.2.3"
  }
}

모노레포 예제 (Turborepo)

다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 구조
monorepo/
├── apps/
   ├── web/
   └── api/
├── packages/
   ├── shared-lib/
   └── utils/
└── turbo.json
# 클론 (한 번에 모든 것)
git clone https://github.com/org/monorepo.git
# 빌드 (의존성 자동 해결)
npm install
npm run build
# 변경사항 (원자적 커밋)
git add packages/shared-lib apps/web
git commit -m "feat: update shared-lib and use in web"

선택 가이드

Submodule 선택:

  • ✅ 독립적인 릴리스 주기
  • ✅ 레포별 권한 분리 필요
  • ✅ 여러 프로젝트에서 동일 라이브러리 사용
  • ❌ 팀이 Git 초보 패키지 선택:
  • ✅ 버전 관리 중요
  • ✅ 퍼블릭 또는 프라이빗 레지스트리 사용
  • ✅ 자동 업데이트 원함
  • ❌ 소스 코드 직접 수정 필요 모노레포 선택:
  • ✅ 여러 패키지 동시 수정 빈번
  • ✅ 원자적 변경 중요
  • ✅ 공통 CI/CD 파이프라인
  • ❌ 레포 크기 제한 (수십 GB)


실무 사례

1. 공유 프로토콜 버퍼 레포

구조: 아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

api-gateway/
├── src/
└── proto/  (서브모듈 → shared-proto-repo)
user-service/
├── src/
└── proto/  (서브모듈 → shared-proto-repo)
order-service/
├── src/
└── proto/  (서브모듈 → shared-proto-repo)

워크플로우: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 1. shared-proto-repo에서 스키마 변경
cd shared-proto-repo
git checkout -b feature/add-user-field
# user.proto 수정
git commit -m "feat: add email field to User"
git push origin feature/add-user-field
# 2. PR 머지 후 각 서비스 업데이트
cd api-gateway
git submodule update --remote proto
git add proto
git commit -m "chore: update proto to include email field"

2. 공유 UI 컴포넌트 라이브러리

구조: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

web-app/
├── src/
└── components/  (서브모듈 → shared-components)
admin-app/
├── src/
└── components/  (서브모듈 → shared-components)

버전 고정 전략: 다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 안정 버전 사용 (태그)
cd components
git fetch --tags
git checkout v2.1.0
cd ..
git add components
git commit -m "chore: pin components to v2.1.0"
# 최신 개발 버전 사용 (브랜치)
cd components
git checkout develop
git pull origin develop
cd ..
git add components
git commit -m "chore: update components to latest develop"

3. 문서 레포 분리

구조: 다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

website/
├── src/
├── public/
└── docs/  (서브모듈 → documentation-repo)

자동 업데이트 (GitHub Actions): 다음은 yaml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

name: Update Docs
on:
  repository_dispatch:
    types: [docs-updated]
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true
          token: ${{ secrets.PAT }}
      
      - name: Update docs submodule
        run: |
          git submodule update --remote docs
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add docs
          git commit -m "chore: update docs" || exit 0
          git push

4. CI 캐싱 전략

GitHub Actions: 다음은 yaml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      
      - name: Cache submodules
        uses: actions/cache@v3
        with:
          path: |
            .git/modules
            vendor/shared-lib
          key: submodules-${{ hashFiles('.gitmodules') }}-${{ hashFiles('vendor/shared-lib/.git/HEAD') }}
      
      - name: Build
        run: make build

GitLab CI: 아래 코드는 yaml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

build:
  cache:
    key: 
      files:
        - .gitmodules
    paths:
      - .git/modules
      - vendor/
  
  script:
    - git submodule sync
    - git submodule update --init --recursive
    - make build


트러블슈팅

증상원인대응
빈 디렉터리submodule 미초기화submodule update --init --recursive
detached HEADsubmodule은 포인터만 따라감작업 시 브랜치 생성·명시적 push
권한 오류CI 토큰에 서브레포 접근 없음machine user·deploy key·조직 SSO
충돌 merge상위·하위 동시 변경하위에서 먼저 정리 후 상위 SHA 갱신
버전 불일치일부만 pull문서에 “항상 이 명령” 고정
“서브모듈 안에서 실수로 push” 방지: 하위 저장소에 branch protection을 두고, 상위 bump는 PR로만 받습니다.

마무리

Git submodule멀티레포 의존성을 Git 네이티브로 표현하는 도구이며, 성공하려면 클론·업데이트·CI를 레포 README에 한 줄이라도 표준 명령으로 박아 두는 것이 중요합니다. 원격 협업과 Actions 패턴은 Git push pull·원격 협업, 충돌 해결 사고례는 Git merge conflict 실전과 함께 보면 좋습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3