핵심 정리
# 서브모듈 추가
git submodule add https://github.com/org/shared-lib libs/shared-lib
# 저장소 클론 후 서브모듈 초기화 및 체크아웃
git clone https://github.com/org/my-project
git submodule update --init --recursive
# 서브모듈을 최신 커밋으로 업데이트
git submodule update --remote --merge
# 상위 저장소에 서브모듈 포인터 변경 반영
git add libs/shared-lib
git commit -m "chore: shared-lib 최신 버전으로 업데이트"기본 흐름
어떤 외부 저장소 포함 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 외부 저장소를 특정 커밋으로 고정 포함 | submodule |
| clone 후 서브모듈까지 같이 받기 | --recurse-submodules |
| 기존 clone에서 초기화 | git submodule update --init --recursive |
| 외부 코드 병합 관리 단순화 | subtree 검토 |
서브모듈은 외부 저장소를 특정 커밋에 고정해 포함한다 — .gitmodules와 SHA 포인터가 핵심이다
git submodule add를 실행하면 두 가지가 생긴다. .gitmodules 파일에 서브모듈의 URL과 로컬 경로가 기록되고, 상위 저장소의 트리 객체에 해당 디렉터리가 특수한 "gitlink" 항목으로 등록된다. gitlink는 서브모듈 저장소의 특정 커밋 SHA를 가리키는 포인터다. 상위 저장소는 서브모듈의 파일 내용을 직접 저장하지 않는다. 대신 "이 경로의 서브모듈은 SHA a3f8c12를 체크아웃해야 한다"는 정보만 가진다. 덕분에 공유 라이브러리의 버전을 프로젝트마다 다르게 고정할 수 있고, 서브모듈 저장소에 기여하지 않는 한 그 내부 히스토리 전체를 받을 필요가 없다.
# .gitmodules 파일 내용 예시
# [submodule "libs/shared-lib"]
# path = libs/shared-lib
# url = https://github.com/org/shared-lib
# 상위 저장소가 저장하는 것: SHA 포인터
git ls-tree HEAD libs/shared-lib
# 160000 commit a3f8c12d... libs/shared-lib
# ↑ "160000"은 gitlink를 의미하는 특수 모드클론 후 서브모듈은 자동 체크아웃되지 않는다 — --init --recursive가 필요한 이유
git clone은 기본적으로 상위 저장소의 파일만 내려받는다. 이때 서브모듈 작업 디렉터리는 바로 사용 가능한 상태가 아닐 수 있다. .git/config에 서브모듈 항목이 등록되지 않았기 때문에 git submodule update만으로는 기대한 대로 동작하지 않는 경우가 많다. --init은 .gitmodules를 읽어 .git/config에 서브모듈을 등록하는 단계이고, --recursive는 서브모듈 안에 또 다른 서브모듈이 있을 때 재귀적으로 초기화한다. 두 단계를 하나의 명령으로 합치는 것이 git submodule update --init --recursive다. 팀 온보딩 스크립트나 CI 파이프라인에서는 이 초기화 단계를 명시해 두는 편이 안전하다.
# ❌ clone 직후 서브모듈 디렉터리가 비어 있는 상황
ls libs/shared-lib # (빈 디렉터리)
# ✅ 초기화 + 체크아웃을 한 번에
git submodule update --init --recursive
# 또는 clone 시 옵션으로 처리
git clone --recurse-submodules https://github.com/org/my-project서브모듈은 기본적으로 특정 커밋을 checkout하므로 detached HEAD로 시작하기 쉽다
서브모듈은 상위 저장소가 가리키는 특정 커밋을 그대로 checkout하는 경우가 많아서, 처음 열었을 때 HEAD detached at a3f8c12처럼 보이기 쉽다. 이 상태에서 파일을 수정하고 커밋하면 브랜치가 아닌 HEAD에만 커밋이 달릴 수 있다. 이후 상위 저장소에서 git submodule update를 실행하면 서브모듈이 다른 SHA로 이동하면서, 방금 만든 커밋을 다시 찾기 번거로워질 수 있다. 서브모듈 안에서 실제 개발 작업을 할 때는 먼저 브랜치를 만들거나 기존 브랜치로 전환한 뒤 커밋하는 편이 안전하다.
# 서브모듈 안에서 작업할 때 올바른 순서
cd libs/shared-lib
# ✅ 먼저 브랜치로 전환
git switch main
# 또는 새 브랜치 생성
git switch -c feat/new-feature
# 작업 후 커밋
git add .
git commit -m "feat: 새 기능 추가"
git push origin feat/new-feature
# 상위 저장소로 돌아가 포인터 업데이트
cd ../..
git add libs/shared-lib
git commit -m "chore: shared-lib 포인터 업데이트"git subtree와의 비교 — subtree는 단순하지만 외부 기여가 복잡해진다
subtree는 외부 저장소의 히스토리를 상위 저장소에 직접 병합해 넣는다. 클론하면 별도 초기화 없이 모든 파일이 바로 포함되고, detached HEAD 같은 함정이 없다. 반면 외부 저장소에 변경을 다시 push하려면(git subtree push) 상위 저장소 히스토리에서 관련 커밋을 필터링해야 해서 복잡하다. 서브모듈은 외부 저장소와의 양방향 기여가 목적이거나 버전을 엄밀히 고정해야 할 때 적합하고, subtree는 외부 코드를 "가져와서 내 것으로 포함"하는 단순한 시나리오에 잘 맞는다.
# subtree로 외부 저장소 추가 (히스토리 병합 없이)
git subtree add --prefix=libs/shared-lib \
https://github.com/org/shared-lib main --squash
# subtree 업데이트
git subtree pull --prefix=libs/shared-lib \
https://github.com/org/shared-lib main --squash
# 변경을 외부 저장소로 push (서브모듈과 달리 복잡함)
git subtree push --prefix=libs/shared-lib \
https://github.com/org/shared-lib mainsubmodule과 subtree는 "외부 저장소를 다루는 방식"이 다르다
외부 저장소를 독립된 프로젝트로 계속 기여하고 버전도 엄밀히 고정해야 하면 submodule이 맞습니다. 외부 코드를 그냥 내 저장소 안에 포함해 함께 버전 관리하고 싶다면 subtree가 더 단순합니다. submodule은 clone 후 초기화가 필요하고 detached HEAD 함정이 있지만, 외부 저장소와의 경계가 명확합니다. subtree는 시작은 쉽지만 나중에 외부 저장소로 변경을 돌려보낼 때 더 복잡합니다.
체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 외부 저장소를 특정 버전에 고정해 포함 | git submodule add |
| clone 후 서브모듈 코드가 없을 때 | git submodule update --init --recursive |
| 서브모듈 안에서 코드 수정 | 먼저 브랜치 전환 후 커밋 |
| 서브모듈을 최신 커밋으로 올리기 | git submodule update --remote --merge |
| 단순 포함, 양방향 기여 불필요 | git subtree add --squash |
주의할 점
상위 저장소에서 git pull하면 서브모듈 SHA 포인터만 바뀌고 작업 디렉터리 내용은 기대와 다르게 남아 있을 수 있습니다.
--recurse-submodules 사용 여부와 현재 설정을 확인하고, 필요하면 pull 후 git submodule update --init --recursive를 이어서 실행하는 편이 안전합니다.
# ❌ 포인터만 바뀌고 파일은 이전 상태 그대로
git pull
# ✅ 서브모듈 파일까지 자동으로 업데이트
git pull --recurse-submodules
# 또는 pull 후 수동으로 업데이트
git pull
git submodule update --init --recursive
# git config로 기본값 설정 (권장)
git config --global submodule.recurse truesubmodule.recurse true를 설정해 두면 일부 명령에서 재귀 동작을 기본값처럼 쓸 수 있어 편하지만, 팀 표준으로 쓰기 전에는 실제 워크플로와 Git 버전에서 원하는 동작이 맞는지 확인하는 편이 좋습니다.
cd libs/shared-lib
git commit -m "fix: urgent patch"
# detached HEAD 상태에서 커밋
cd ../..
git submodule update --remote --merge
# 방금 커밋이 어떤 브랜치도 가리키지 않아 나중에 찾기 어려워질 수 있음