가이드

Git submodule: 서브모듈 추가, 업데이트, 삭제 방법

업데이트: · Skillaria.Top

git submodule은 다른 저장소를 내 프로젝트 안에 넣고, 특정 커밋에 고정하는 방식입니다. git submodule add 주소 경로로 추가하고, git clone --recurse-submodules로 받고, git submodule update --remote로 갱신하며, git submodule deinit과 git rm으로 삭제합니다.

git submodule(서브모듈)은 프로젝트 폴더 안에 들어 있는 별도의 Git 저장소입니다. 가장 중요한 특징은 상위 저장소가 서브모듈의 파일이 아니라, 그 저장소의 특정 커밋을 가리키는 참조만 저장한다는 것입니다. 그래서 프로젝트를 클론하는 모든 사람은 여러분이 명시적으로 바꾸기 전까지 같은 버전의 서브모듈을 받습니다.

서브모듈은 패키지 관리자 없이 외부 라이브러리를 붙일 때, 여러 프로젝트가 공통 코드를 나눠 쓸 때, 테마나 펌웨어처럼 자체 히스토리가 있는 것을 별도 저장소로 분리할 때 씁니다.

git submodule의 구조

서브모듈은 세 부분으로 이루어집니다. 이 관계를 이해하면 대부분의 문제가 풀립니다.

  • 파일 .gitmodules — 프로젝트 루트에 있으며 커밋됩니다. 어떤 경로가 어떤 URL에서 오는지 적습니다;
  • gitlink 항목 — 커밋 트리 안의 특수한 항목으로, 모드 160000을 가지며 내용 대신 서브모듈 커밋의 해시를 담습니다;
  • 서브모듈 저장소 자체 — 데이터는 상위 저장소의 .git/modules/<이름>에 있고, 서브모듈 폴더에는 그곳을 가리키는 .git 파일이 있습니다.
cat .gitmodules
[submodule "libs/ui-kit"]
	path = libs/ui-kit
	url = https://github.com/example/ui-kit.git

git ls-tree HEAD libs/
160000 commit 8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c	libs/ui-kit

상위 저장소는 브랜치가 아니라 정확한 커밋을 기록한다는 점에 주목하세요. 이것이 서브모듈과 "최신 main 버전을 따른다"는 식의 의존성이 근본적으로 다른 이유입니다.

서브모듈 추가하기

git submodule add https://github.com/example/ui-kit.git libs/ui-kit
Cloning into '/home/dev/shop/libs/ui-kit'...
done.

git status
	new file:   .gitmodules
	new file:   libs/ui-kit

git commit -m "ui-kit을 서브모듈로 연결"

명령은 저장소를 클론하고, .gitmodules를 만들거나 갱신하고, gitlink를 인덱스에 추가합니다. 남은 일은 커밋뿐입니다. 경로를 생략하면 저장소 이름으로 폴더가 만들어집니다.

업데이트 때 특정 브랜치를 따르게 하려면 추가할 때 지정하세요: git submodule add -b develop <url> <경로>. 그러면 .gitmodules에 branch = develop이 기록됩니다.

상대 URL

서브모듈이 같은 서버에 있다면 상대 경로를 쓸 수 있습니다: git submodule add ../ui-kit.git libs/ui-kit. 상위 저장소의 원격 URL을 기준으로 해석되므로, 프로젝트를 SSH로 클론했든 HTTPS로 클론했든 동작합니다.

서브모듈과 함께 프로젝트 클론하기

가장 흔한 불만은 "클론했는데 서브모듈 폴더가 비어 있어요"입니다. 기본적으로 git clone은 서브모듈을 받지 않습니다. 올바른 방법은 다음과 같습니다.

git clone --recurse-submodules https://github.com/example/shop.git

이미 서브모듈 없이 클론했다면:

git submodule update --init --recursive

--init은 .gitmodules의 서브모듈을 로컬 .git/config에 등록하고, update는 이를 내려받아 고정된 커밋으로 맞춥니다. --recursive는 서브모듈 안의 서브모듈에도 같은 작업을 합니다.

서브모듈 업데이트하기

여기서는 두 가지 일을 구분해야 합니다. "동료가 커밋한 버전을 받는 것"과 "라이브러리의 새 버전으로 옮기는 것"입니다.

프로젝트에 고정된 버전 받기

git pull 후 상위 저장소의 gitlink는 바뀌었을 수 있지만, 서브모듈 파일은 저절로 바뀌지 않습니다. git status에 modified: libs/ui-kit (new commits)가 나타납니다. 맞춰 주세요.

git pull
git submodule update --init --recursive

매번 손으로 하지 않으려면 자동화를 켜세요: git config submodule.recurse true. 그러면 pull, checkout, switch가 서브모듈도 함께 갱신합니다. 일회성으로는 git pull --recurse-submodules를 쓰면 됩니다.

라이브러리의 새 버전으로 옮기기

# 방법 1: 추적 중인 브랜치의 최신 커밋 가져오기
git submodule update --remote libs/ui-kit

# 방법 2: 버전을 직접 골라 설정하기
cd libs/ui-kit
git fetch
git checkout v3.2.0
cd ../..

# 두 경우 모두: 상위 저장소에서 새 버전을 기록
git add libs/ui-kit
git commit -m "ui-kit을 v3.2.0으로 업데이트"

--remote는 .gitmodules의 branch 값을 쓰고, 값이 없다면 서브모듈 원격 저장소의 HEAD가 가리키는 브랜치(보통 main)를 씁니다. 상위 저장소에 커밋하지 않으면 동료들은 새 버전을 받지 못합니다. 서브모듈 업데이트는 항상 gitlink 변경이므로 커밋해야 합니다.

서브모듈의 변화를 보는 방법:

git diff --submodule=log
git config diff.submodule log    # 이 형식을 기본으로
git submodule status
 8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c libs/ui-kit (v3.1.0)
+1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b libs/charts (v1.4.0-3-g1a2b3c4)

git submodule status의 접두사: -은 서브모듈이 초기화되지 않음, +는 현재 커밋이 상위 저장소에 기록된 것과 다름, U는 병합 충돌을 뜻합니다.

서브모듈 안에서 변경하기

git submodule update 후 서브모듈은 detached HEAD 상태입니다. 브랜치가 아니라 특정 커밋에 있는 것입니다. 이 상태에서 바로 커밋하면 다음 update 때 그 커밋을 잃기 쉽습니다. 다음 순서로 작업하세요.

cd libs/ui-kit
git switch main                  # 브랜치로 이동
# ... 수정 ...
git commit -am "버튼 여백 수정"
git push                         # 먼저 서브모듈을 push!
cd ../..
git add libs/ui-kit
git commit -m "ui-kit: 버튼 여백 수정 반영"
git push

push 순서가 중요합니다. 상위 저장소를 서브모듈보다 먼저 올리면, 서버의 프로젝트는 여러분만 내려받을 수 있는 커밋을 가리키게 되고, 동료들의 git submodule update는 fatal: remote error: upload-pack: not our ref나 did not contain 같은 오류로 실패합니다. git push --recurse-submodules=check는 서브모듈이 올라가지 않았다면 push를 거부하고, =on-demand는 서브모듈을 먼저 자동으로 올립니다.

서브모듈 삭제하기

예전에는 여러 파일을 손으로 고쳐야 했지만, 최신 Git에서는 세 단계로 충분합니다.

git submodule deinit -f libs/ui-kit   # .git/config에서 빼고 폴더 비우기
git rm -f libs/ui-kit                 # gitlink와 .gitmodules 항목 제거
rm -rf .git/modules/libs/ui-kit       # 저장된 서브모듈 데이터 삭제
git commit -m "ui-kit 서브모듈 삭제"

마지막 단계는 정확성에는 필요 없지만, 생략하면 같은 경로에 서브모듈을 다시 추가할 때 Git이 "A git directory for 'libs/ui-kit' is found locally"라며 불평할 수 있습니다.

서브모듈 URL이 바뀌었을 때

저장소를 옮겼다면 .gitmodules를 고치고 로컬 설정을 동기화합니다.

git submodule set-url libs/ui-kit https://gitlab.com/example/ui-kit.git
git submodule sync --recursive
git commit -am "ui-kit의 새 주소"

동료들도 pull 후에 git submodule sync를 실행해야 합니다. .gitmodules의 URL은 init이나 sync를 할 때만 각자의 .git/config에 복사되기 때문입니다.

여러 서브모듈을 다룰 때 쓸 명령

작업명령
목록과 상태git submodule status --recursive
각 서브모듈에서 명령 실행git submodule foreach 'git switch main && git pull'
다운로드 가속git submodule update --init --jobs 4
히스토리 없이 내려받기git submodule update --init --depth 1
status에서 요약 보기git config status.submoduleSummary true

서브모듈에서 자주 생기는 문제

  • 클론 후 빈 폴더 — init을 하지 않았습니다. 해결: git submodule update --init --recursive.
  • pull 직후 "modified: (new commits)" — 상위 저장소는 다른 커밋을 가리키는데 파일은 옛것입니다. 해결: git submodule update.
  • 서브모듈을 실수로 되돌림 — 서브모듈이 옛 커밋에 있을 때 git add .를 해서 버전을 거꾸로 커밋했습니다. 커밋 전에 항상 git diff --submodule을 확인하세요.
  • 병합 시 gitlink 충돌 — 두 브랜치가 서브모듈을 서로 다른 커밋으로 올렸습니다. 서브모듈에 들어가 두 변경을 모두 포함하는 커밋(보통 더 새 것)을 고르고, git add 경로를 한 뒤 병합을 마무리하세요.
  • CI에서 비공개 서브모듈 — 접근 권한이 없어 빌드가 서브모듈을 받지 못합니다. CI에 서브모듈 저장소 접근용 키나 토큰을 주어야 합니다. 상대 URL을 쓰면 설정이 쉬워집니다.

팀에서 지킬 규칙

서브모듈 문제의 대부분은 Git 자체보다 사람들이 제각각 행동하는 데서 옵니다. 프로젝트 README에 다음 규칙을 적어 두세요.

  1. 항상 --recurse-submodules로 클론하고, 모두 submodule.recurse true를 설정합니다.
  2. 서브모듈 업데이트는 별도 커밋으로 하고, 어떤 버전인지와 이유를 메시지에 씁니다.
  3. 상위 프로젝트에서 서브모듈 코드를 고치지 않습니다. 고쳐야 한다면 서브모듈 자체에 커밋하고 push한 뒤 반영합니다.
  4. CI에서는 git submodule update --init --recursive를 명시적으로 실행하고, 빌드가 로컬 변경에 의존하지 않는지 확인합니다.

CI에서 서브모듈 커밋이 모두 접근 가능한지 확인하는 가장 간단한 방법은 깨끗한 클론입니다. 누군가 서브모듈 push를 잊었다면, 동료가 일주일 뒤에 발견하는 대신 CI의 update 단계에서 바로 실패합니다.

git clone --recurse-submodules --jobs 4 "$REPO_URL" build
cd build
git submodule status --recursive

서브모듈의 대안

서브모듈은 정확한 버전 통제와 분리된 히스토리를 주지만, 각 참여자에게 수동 단계를 더합니다. 코드가 평범한 라이브러리라면 패키지 관리자(npm, pip, Composer, Go modules)가 더 간단합니다. 외부 코드가 명령 없이 그냥 클론되기를 원한다면 git subtree를 고려하세요. 이 방식은 히스토리를 메인 저장소에 복사해 넣으므로, 동료가 알아야 할 것이 없습니다. 연관된 서비스가 많은 팀은 모노레포를 선택하는 경우가 많습니다. 서브모듈은 내부 코드가 독자적인 삶을 가질 때, 즉 자체 릴리스와 자체 팀이 있을 때 정당화됩니다.

정리

git submodule은 다른 저장소의 특정 커밋을 가리키는 참조입니다. 추가는 git submodule add, 받기는 --recurse-submodules 또는 update --init, 새 버전 적용은 update --remote와 상위 저장소의 커밋, 삭제는 deinit, git rm, .git/modules 정리입니다. 서브모듈 안의 detached HEAD와 push 순서를 기억하세요. 고급 Git 도구는 Git 강의에서 실습으로 다룹니다.

자주 묻는 질문

서브모듈이 항상 detached HEAD인 이유는 무엇인가요?

상위 저장소는 브랜치가 아니라 커밋 해시를 저장하고, git submodule update는 서브모듈을 바로 그 커밋으로 옮깁니다. 수정하려면 브랜치로 직접 전환하거나 git submodule update --remote --merge를 사용하세요.

서브모듈을 태그에 고정할 수 있나요?

직접은 안 됩니다. 하지만 서브모듈에서 git checkout으로 태그에 옮긴 뒤, 그 커밋을 상위 저장소에 커밋하면 됩니다. git submodule status에서 태그가 괄호 안에 보입니다.

GitHub의 ZIP 다운로드에도 서브모듈이 들어 있나요?

아니요. GitHub의 소스 아카이브에는 서브모듈 내용이 없어서 폴더가 비어 있습니다. git clone --recurse-submodules를 쓰세요.

git submodule과 git subtree는 무엇이 다른가요?

submodule은 외부 저장소의 커밋을 가리키고 코드를 받으려면 별도의 명령이 필요합니다. subtree는 파일과 히스토리를 메인 저장소 안에 직접 복사합니다.

서브모듈을 다른 폴더로 옮길 수 있나요?

네, git mv 이전경로 새경로를 실행하면 Git이 .gitmodules와 내부 참조를 함께 갱신합니다. 이후 커밋만 하면 됩니다.

영상으로 이 주제 배우기

이 주제는 “Git 완전 정복: 초보에서 능숙한 개발자로” 강의에서 실습과 함께 자세히 다룹니다.

강의로 이동