일.배경
종종 큰 코드베이스 (repo) 를 여러 개의 작은 repo 로 분할하고 싶은 시나리오에 직면합니다. 예를 들어:
-
기존 코드베이스가 방대하고 모듈 관리가 혼란스러워 종종 다른 사람의 것을 실수로 수정하기 쉽다
-
특정 모듈을 별도로 빌드해야 한다. 예를 들어 jQuery 프로젝트 내의 React 파일럿, Node 프로젝트 내의 순수 프론트엔드 부분, Electron 프로젝트 내의 UI 부분 등
-
특정 모듈이 블랙박스 의존성 항목이며, 개발 중에는 빌드 후 버전에만 의존한다. 예를 들어 프레임워크 라이브러리 등
이러한 상황에 대해 일반적으로 3 가지 해결책이 있습니다:
-
npm package: 의존성 항목을 npm package 로 분리하고 코드베이스도 독립적으로 분리한다
-
monorepo: 단일 repo 의 크기가 방대해도 관계없다. 모듈별로 관리하면 된다
-
git submodules: 의존성 항목을 여러 개의 독립된 repo 로 분리하고 주 repo 의 submodule 로 관리한다
npm package
npm package 의 장점은 성숙한 의존성 관리 메커니즘에 있으며, 규범적이고 사용하기 쉽습니다. 단점은 주 프로젝트가 패키지 버전 번호를 통해 독립 모듈의 업데이트를 가져올 수밖에 없다는 점이며, 주 프로젝트가 하위 모듈과 연동하여 디버깅해야 하는 시나리오에서는 매우 번거롭습니다:
주 프로젝트: 작동하지 않네요
하위 모듈: 문제가 있습니다, 수정하겠습니다...버전 번호 변경 - 빌드-npm package 게시
주 프로젝트: 의존성 업데이트, 재시도...여전히 작동하지 않네요
하위 모듈: 아직 문제가 있습니다...
빈번하게 버전을 릴리스하는 것은 어리석습니다. 로컬에서 수정하여 빌드한 후 복사해 갈 수 있지만 여전히 번거롭습니다. 물론, 일반적으로 mock 인터페이스 또는 데이터를 통해 연동 디버깅 의존성을 분리할 수 있지만, 때로는 API 전체를 mock 하는 비용이 높고, 가짜는 진짜만큼 사용하기 좋지 않습니다
monorepo
monorepo 는 repo 를 분할하지 않고 단일 repo 내에서 각 모듈의 빌드 플로우, 버전 번호 등을 통일 관리할 것을 주장하며, 다른 사람의 코드를 수정하는 것도 장려합니다
이는 모듈 경계가 명확하고 owner 가 명확한 프로젝트 (React, Babel 등) 에 적합하지만, 실제 애플리케이션에서는 비즈니스 repo 는 명확한 모듈 경계와 의존 관계를 유지하기 어렵습니다. 이때 monorepo 는 지나치게 이상화됩니다
git submodules
git submodules 는 npm package 와 유사한 의존성 관리 메커니즘을 제공합니다. 의존성 항목 추가, 삭제, 업데이트 등의 기능을 포함합니다. 차이는 전자가 관리하는 의존성은 하위 모듈의 소스 코드이고, 후자가 관리하는 것은 하위 모듈의 빌드 결과물이라는 점입니다. 이 점에서 git submodules 는 monorepo 와 일치합니다 (둘 다 하위 모듈의 소스 코드를 신경 씁니다)
이로 인해 주 프로젝트가 하위 모듈과 빈번하게 연동 디버깅해야 할 때의 번거로움이 사라집니다. 주 프로젝트가 가져오는 submodules 는 완전한 repo 이므로 직접 수정 - 빌드 - 커밋할 수 있기 때문입니다
이.submodules 와 monorepo
구조적으로 보면, submodules 프로젝트의 주 repo 는 monorepo 와 유사합니다. monorepo 내의 각 모듈을 독립된 repo 로 추출한 것과 같으며, 주 repo 가 의존하는 각 모듈의 버전 번호 (commit hash 형식) 만 기록합니다
구체적으로, monorepo 는 단일 repo 내에 모든 하위 모듈의 소스 코드 (packages/xxx/src) 를 저장합니다. 예를 들어:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
반면, submodules 는 주 repo 내에 모든 하위 모듈의 "인덱스"만 저장합니다 (repo url + branch name + commit hash). 예를 들어:
# 주 repo 의.gitmodules 파일
[submodule "react-dom"]
path = packages/react-dom
url = https://github.com/facebook/react.git
branch = master
[submodule "react-reconciler"]
path = packages/react-reconciler
url = https://github.com/facebook/react.git
branch = master
...
주 repo 내에는 해당 빈 디렉토리를 하위 모듈의 "구멍"으로 유지하며, 소스 코드는 저장하지 않습니다:
react/
packages/
react-dom/ # 빈 디렉토리
react-reconciler/ # 빈 디렉토리
모든 submodules 의존성을 가져온 후 실제 디렉토리 구조는 다음과 같습니다:
react/
packages/
react-dom/
/src
react-reconciler/
/src
...
주 repo 는 하위 모듈의 소스 코드를 추적하지 않으며, 그 버전 번호만 기록합니다 (commit hash 형식):
# 빈을 출력하여 하위 모듈 src 를 추적하지 않음을 나타냄
$ git ls-tree -r master | grep packages/react-dom/src
# 주 repo 내에서 git 에 추적되는 하위 모듈 구멍 위치 확인
$ git ls-tree -r master | grep ' commit'
160000 commit 3edf340cee50fd4bc918a0a95b438a30447ae042 packages/react-dom
160000 commit 373f207b09a7bf900fa82c3188aeefdc9ce6146c packages/react-reconciler
...
P.S.git ls-tree의 출력 형식 의미는 Output Format 참조
삼.구체적인 사용법
git submodule 명령은 하위 모듈을 관리하는 데 사용합니다:
$ git submodule --help
git-submodule - Initialize, update or inspect submodules
# 초기화
git submodule init
# 추가
git submodule add
# 삭제
git submodule deinit
# 변경 (버전 관리)
git submodule update
하위 모듈 의존성 추가
$ cd ./react
# 의존성 추가
$ git submodule add -b master https://github.com/path-to/react-dom.git src/packages/react-dom
주 repo 에 src/packages/react-dom 빈 디렉토리를 생성하여 하위 모듈의 구멍 위치로 합니다. 실제로 add 프로세스에서는 주로 3 가지 일이 발생합니다:
-
하위 모듈 repo 를 주 repo 의 git 캐시 디렉토리에 clone 합니다. 예를 들어
.git/modules/src/packages/react-dom -
구멍 위치 빈 디렉토리를 생성하고 하위 모듈 repo 의 최신 commit hash 와 연결합니다
-
주 repo 의 루트 디렉토리에 필요에 따라
.gitmodules파일을 생성하여 하위 모듈 repo 주소 (url), 브랜치 이름 (branch), 구멍 위치 패스 (path) 를 기록합니다
그런 다음 이러한 하위 모듈 설정을 커밋합니다:
$ git add ./src/packages/react-dom ./.gitmodules
$ git commit -m "build: add react-dom submodule"
$ git push origin master
다음으로 로컬에서 하위 모듈을 가져와 초기화를 완료합니다:
# 하위 모듈 초기화
$ git submodule update --init
하위 모듈 repo 를 src/packages/react-dom 디렉토리에 clone 합니���. 실제로는 2 가지 일이 발생합니다:
-
캐시가 존재하는지 확인하고 clone 된 하위 모듈 repo 가 있는지 확인합니다 (예를 들어 clone 된 주 repo 가
add되지 않은 경우 캐시가 존재하지 않음). 필요에 따라 clone -
하위 모듈 repo 의 루트 디렉토리에
.git/config를 생성하여 해당 repo 주소 (url) 를 기록합니다
하위 모듈 초기화
submodules 가 포함된 repo 를 clone 한 후 초기화를 수행해야 합니다:
# 일부 로컬 설정 생성
$ git submodule init
# 각 하위 모듈 repo 가져오기
$ git submodule update --init
또는 주 repo 를 clone 할 때 --recursive 옵션을 사용하여 위 2 단계 작업을 완료할 수도 있습니다:
$ git clone git://gihub.com/path-to/main-repo.git --recursive
하위 모듈 업데이트 가져오기
모든 하위 모듈을 업데이트합니다:
$ git submodule update --remote
하위 모듈의 해당 브랜치 최신 코드를 가져옵니다. 업데이트가 있는 경우 플레이스홀더 디렉토리의 git 상태가 변경됩니다:
$ git status
modified: src/packages/react-dom (new commits)
실제로는 commit hash 가 변경되었습니다:
$ $ git diff
diff --git a/src/packages/react-dom b/src/packages/react-dom
index 3edf340cee..d056efbc62 160000
--- a/src/packages/react-dom
+++ b/src/packages/react-dom
@@ -1 +1 @@
-Subproject commit 3edf340cee50fd4bc918a0a95b438a30447ae042
+Subproject commit d056efbc62cbf976b4ef83e70d7019fba4506e85
P.S.submodules 내의 commit hash 는 npm package 의 dependencies 버전 번호에 해당합니다
의존성 항목 버전 제어
주 repo 가 의존하는 하위 모듈의 버전을 업데이트하려면 이 commit hash 변경을 커밋합니다:
$ git add src/packages/react-dom
$ git commit -m "build: update react-dom submodule"
$ git push origin master
그렇지 않은 경우 --remote 옵션을 붙이지 않으면 현재 의존 버전으로 되돌아갑니다:
$ git submodule update
하위 모듈 코드 변경
하위 모듈은 독립된 repo 이므로 정상적으로 조작합니다:
$ cd ./packages/react-dom
# 브랜치 전환에 주의. 일반적으로 detached 상태
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
그 후 주 repo 는 git submodule update --remote 를 통해 최신 버전을 가져올 수 있으며, 주 repo 가 의존하는 하위 모듈 버전을 업그레이드할지 여부를 결정할 수 있습니다
각 하위 모듈에 대해 동일한 git 명령 실행
foreach 명령을 제공하여 하위 모듈에 대해 배치 처리를 수행합니다. 예를 들어:
# 각 하위 모듈 디렉토리로 이동하여 git stash 실행
$ git submodule foreach 'git stash'
# 통일하여 feature 브랜치 생성
$ git submodule foreach 'git checkout -b featureA'
여러 하위 모듈 의존성이 있을 때 이 명령은 매우 유용합니다
P.S.submodules 의 더 많은 사용법은 7.11 Git Tools - Submodules 참조
사.자주 발생하는 문제
하위 모듈 브랜치가 detached 상태에 있음
git submodule update --remote 를 실행할 때마다 하위 모듈은 detached 상태가 됩니다. 예를 들어:
$ cd ./packages/react-dom
$ git branch
* (HEAD detached at ac4d1fc)
master
설계상如此이며, 좋은 해결책이 없습니다:
It's also important to realize that a submodule reference within the host repository is not a reference to a specific branch of that submodule's project, it points directly to a specific commit (or SHA1 reference), it is not a symbolic reference such as a branch or tag. In technical terms, it's a detached HEAD pointing directly to the latest commit as of the submodule add.
따라서 하위 프로젝트 코드를 변경하기 전에 수동으로 master 브랜치로 전환해야 합니다:
$ git checkout master
$ git add .
$ git commit -m 'feat: xxx'
$ git push origin master
로컬 하위 모듈 캐시
하위 모듈 repo 가 이전发生时, git submodule add 를 실행하면 로컬 캐시 문제에 직면할 수 있습니다:
$ git submodule add ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git projets/fdf
A git directory for 'projets/fdf' is found locally with remote(s): origin ssh://git @XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git If you want to reuse this local git directory instead of cloning again from ssh://XXX.XXX.XXX.XXX:XXXXX/opt/git/fdf.git use the '--force' option. If the local git directory is not the correct repo or you are unsure what this means choose another name with the '--name' option.
원래 설정 (2, 3 단계) 을 삭제한 후 로컬 캐시된 하위 모듈 정보 (1, 4 단계) 를 삭제해야 합니다:
# 1.git 캐시 및 물리적 파일 삭제
$ git rm --cached path_to_submodule
$ rm -rf path_to_submodule
# 2..gitmodules 내의 해당 하위 모듈 관련 설정 삭제
$ vi .gitmodules
[submodule "path_to_submodule"]
path = path_to_submodule
url = https://github.com/path_to_submodule
# 3..git/config 내의 해당 하위 모듈 관련 설정 삭제
$ vi .git/config
[submodule "path_to_submodule"]
url = https://github.com/path_to_submodule
# 4. 하위 모듈 캐시 삭제
$ rm -rf .git/modules/path_to_submodule
정리 완료 후 다시 git submodule add 를 실행합니다
P.S.4 단계에서 하위 모듈의 캐시 위치는 다음 명령으로 확인할 수 있습니다:
$ cat path_to_submodule/.git
gitdir: ../.git/modules/path_to_submodule
P.S.더 많은 자주 발생하는 문제는 Using Git Submodules 참조
아직 댓글이 없습니다