매일공부

[Git] 새 브랜치로 만든 기능, 테스트부터 dev 실반영까지 본문

IT/작업기록

[Git] 새 브랜치로 만든 기능, 테스트부터 dev 실반영까지

julian 2026. 8. 18. 13:13

배경

기존 화면(예: 문서 양식) 하나를 새로 디자인해보고 싶어서, master를 직접 건드리지 않고 feature/xxx-redesign 같은 새 브랜치를 따서 작업했다. 문제는 이 프로젝트의 dev 환경이 "컨테이너 하나가 특정 디렉터리를 마운트해서 그 코드를 그대로 서빙하는" 구조라는 점이었다 — 브랜치를 dev에 push하면 지금 다른 용도로도 쓰이고 있는 그 dev 서버 자체가 이 실험용 브랜치로 바뀌어버리는 게 아닌지 걱정됐다.

Q. 이 브랜치를 그대로 dev에 push하면 안 되는 거 아닌가?

확인해보니 걱정할 필요는 없었다. 구조를 짚어보면:

  • dev 컨테이너가 마운트하는 디렉터리는, 지금 체크아웃되어 있는 브랜치의 코드를 그대로 보여준다
  • dev 리모트로 push하면 pre-receive 훅이 동작해서 push된 커밋을 임시 워크트리에 풀어 pytest/mypy를 돌리고, 통과해야만 push가 받아들여진다
  • 하지만 이 훅은 검사만 한다. 워킹트리(실제 마운트된 디렉터리)가 자동으로 갱신되는 건, 지금 체크아웃되어 있는 브랜치로 push할 때만이다 (git의 receive.denyCurrentBranch=updateInstead 설정에 의한 동작)

즉 평소 master가 체크아웃되어 있는 상태에서 feature/xxx-redesign 브랜치를 push하면:

  • 자동 테스트(pytest/mypy)는 그 브랜치 기준으로 돌아간다
  • 하지만 실제로 dev 서버가 보여주는 코드(master)는 전혀 바뀌지 않는다

push만으로는 반대로 실제 화면을 눈으로 확인할 수도 없다 — 자동 테스트 통과 여부만 확인 가능하고, 실제 렌더링 결과(레이아웃, PDF 등)를 보려면 별도 절차가 필요하다.

실제로 화면을 확인하려면

  1. git push dev feature/xxx-redesign — master는 안 건드리고, pytest/mypy 자동검사가 먼저 통과하는지 확인
  2. NAS의 dev 디렉터리에 터미널로 접속 (컨테이너 안에서 작업)
  3. git status로 미커밋 변경사항이 없는지 확인
  4. 그 저장소 안에서 브랜치를 직접 전환: git checkout feature/xxx-redesign
  5. (필요하면) python manage.py migrate 등 후속 작업 실행
  6. 브라우저로 dev 서버 접속해서 실제 화면 확인
  7. 확인 끝나면 반드시 git checkout master로 원복 — 다른 작업에도 같이 쓰이는 dev 서버를 실험용 브랜치 상태로 방치하면 다른 작업에 지장을 줄 수 있다

컨테이너 안에서 또 만난 "dubious ownership" 에러

브랜치를 전환하려는데 이런 에러가 났다.

fatal: detected dubious ownership in repository at '/app/<project>'
To add an exception for this directory, call:
        git config --global --add safe.directory /app/<project>

이건 컨테이너 안의 git이 마운트된 디렉터리의 소유자가 다르다고 판단해서 막아둔, git의 기본 안전장치일 뿐이다. 안내된 대로 처리하면 된다.

git config --global --add safe.directory /app/<project>

이 설정은 컨테이너 안에서만 적용되는 git 전역 설정이라 코드/데이터에는 영향이 없다.

브랜치 전환할 때 헷갈렸던 것 — fetch dev가 안 먹힘

컨테이너 안에서 git fetch dev를 실행했는데 이런 에러가 났다.

fatal: 'dev' does not appear to be a git repository

이유는 간단했다 — 여기가 바로 dev 리모트가 가리키는 그 저장소 자체였다. 로컬(개발 PC)에서 dev라는 리모트로 push하면 바로 이 디렉터리로 들어오는 구조이니, 이 안에서 다시 dev라는 리모트를 찾을 이유가 없다. push한 브랜치는 이미 이 저장소 안에 로컬 브랜치로 들어와 있는 상태였다.

git branch -a

로 확인해보니 push했던 feature/xxx-redesign이 로컬 브랜치 목록에 이미 있었고, fetch 없이 바로 전환하면 됐다.

git checkout feature/xxx-redesign

정리 — 여기까지 테스트 단계

  • 이 구조에서 dev로 브랜치를 push하는 것과, dev 서버가 실제로 그 브랜치를 서빙하는 것은 완전히 별개다. push는 "CI 검사 통과 + 저장소에 커밋 반영"까지만 하고, 실제 워킹트리 반영은 "지금 체크아웃된 브랜치로 push했을 때"만 자동으로 일어난다
  • 다른 브랜치를 실제로 눈으로 확인하려면, 그 디렉터리 안에서 직접 git checkout으로 전환해야 한다 — 그리고 확인이 끝나면 반드시 원래 브랜치(master)로 되돌려야 한다
  • 컨테이너 안에서 마운트된 저장소를 다룰 때는 "여기가 이미 리모트 그 자체"라는 걸 기억해야 한다 — 로컬 PC에서처럼 fetch dev를 다시 시도할 필요가 없다
  • 테스트 중 실행한 마이그레이션은 브랜치를 원복해도 자동으로 롤백되지 않는다는 점도 함께 기억해둘 것 (컬럼 추가처럼 원래 코드에 영향 없는 변경이라면 당장은 문제 없지만, 인지하고 있어야 한다)

테스트가 끝난 뒤 — master로 병합해서 실제 반영하기

테스트까지만 정리하고 끝내면 반쪽짜리다. 확인이 끝난 기능을 실제로 dev 환경에 반영하는 절차까지 이어서 정리한다.

3. 로컬: master로 병합 → dev로 push (이번엔 실반영)

git checkout master
git merge --no-ff feature/xxx-redesign -m "Merge branch 'feature/xxx-redesign' ..."
git push dev master

이번엔 master가 dev의 현재 체크아웃 브랜치이므로, 검사(pytest/mypy) 통과 시 워킹트리가 자동으로 갱신된다 — 테스트 단계와 다르게 컨테이너에 들어가서 수동으로 checkout할 필요가 없다.

4. dev 컨테이너 안: 반영 확인

git log --oneline -3    # merge 커밋이 보이면 정상 반영
python manage.py migrate   # 혹시 놓친 마이그레이션 적용 (이미 적용된 건 안전하게 skip됨)

5. (선택) 다 쓴 브랜치 정리

git branch -d feature/xxx-redesign
git push dev --delete feature/xxx-redesign

전체 흐름 요약

[로컬] 기능 브랜치 생성 → git push dev feature/xxx  (검사만, 워킹트리 변화 없음)
   ↓
[dev 컨테이너] git checkout feature/xxx 로 직접 전환 → 눈으로 확인 → 끝나면 git checkout master로 원복
   ↓
[로컬] git checkout master → git merge --no-ff feature/xxx → git push dev master  (실반영)
   ↓
[dev 컨테이너] git log로 반영 확인, 필요시 migrate
   ↓
(선택) 로컬/dev 양쪽에서 브랜치 삭제

자주 헷갈리는 git 명령어들

git push가 브랜치를 만들어주는 게 아니다

git push <리모트> <브랜치>는 이미 로컬에 있는 브랜치를 원격으로 보내는 명령이지, 브랜치 자체를 만들어주는 명령이 아니다. 로컬 브랜치는 먼저 git checkout -b로 만들어야 한다.

git checkout -b feature/xxx   # 로컬에 새 브랜치 생성 + 그 브랜치로 이동
git push dev feature/xxx      # 그제서야 dev 원격에 같은 이름의 브랜치가 생김

이미 master에 커밋해버렸을 때 — 브랜치로 분리하기

브랜치를 새로 만들 걸 깜빡하고 master에 그대로 커밋해버린 경우, 아직 아무 데도 push하지 않았다면 그 커밋을 브랜치로 분리해낼 수 있다.

git branch feature/xxx      # 지금 커밋 위치에 새 브랜치를 만듦 (master는 그대로 둔 채)
git reset --hard HEAD~1     # master를 그 커밋 이전 상태로 되돌림
git checkout feature/xxx    # 이제 새 브랜치로 이동해서 작업 계속
git push dev feature/xxx    # dev 원격에 push

git branch <이름>은 지금 위치에 브랜치 포인터만 하나 더 만드는 것이고, git reset --hard HEAD~1로 master만 한 커밋 뒤로 되돌리면 그 커밋은 master에서는 빠지고 새 브랜치에만 남는다.

git checkout은 디스크의 실제 파일을 덮어쓴다

git checkout <브랜치>를 실행하면, 그 브랜치 시점의 파일 내용을 디스크에 실제로 다시 써준다. 이게 별거 아닌 것 같아도 헷갈릴 수 있는 지점이 있다 — 예를 들어 어떤 파일을 미리 다른 곳에 백업해뒀다가 나중에 그 백업으로 복원했는데, 파일의 "수정일자(mtime)"가 백업한 날짜보다 최신으로 찍혀서 당황할 수 있다.

이건 버그가 아니다. git checkout이 파일 내용은 과거 버전으로 되돌리지만, 그 내용을 디스크에 쓰는 시점은 checkout을 실행한 바로 그 순간이기 때문이다. OS 입장에서는 "방금 막 저장된 파일"이니 mtime은 최신으로 찍히고, 내용은 예전 버전인 상태가 정상이다. 파일 내용과 수정일자는 서로 다른 정보라는 걸 헷갈리지 않으면 된다.

브랜치를 완전히 정리할 때 — 로컬/원격 양쪽 다, 순서 주의

브랜치를 다 쓰고 지울 때는 로컬과 원격(dev) 양쪽 다 지워야 한다. 그리고 지금 체크아웃되어 있는 브랜치는 삭제할 수 없으므로, 먼저 다른 브랜치로 이동해야 한다.

# 컨테이너/로컬 양쪽에서 그 브랜치를 체크아웃 중이라면 먼저 벗어나기
git checkout master

# 원격(dev) 브랜치 삭제
git push dev --delete feature/xxx

# 로컬 브랜치 삭제 (master에 merge 안 된 상태라면 -d 대신 -D 필요)
git branch -D feature/xxx

merge하지 않고 그냥 브랜치만 지우면, 그 브랜치에만 있던 커밋은 (다른 브랜치에 포함되지 않는 한) 사실상 사라진다 — 얼마간은 git reflog로 흔적이 남아있어 복구 가능성이 있지만, 시간이 지나거나 정리(gc)가 되면 완전히 사라질 수 있다. "이 브랜치는 테스트만 하고 버릴 것"이라는 판단이 서면 이 방식이 깔끔하고, "나중에도 재사용할 도구"라면 지우지 말고 master에 merge해서 남겨두는 쪽을 추천한다.

Comments