이 장이 끝나면: 온티켓의 변경이 브랜치 → 커밋 → PR의 흐름으로 관리되고, "테스트가 실패하면 커밋이 안 되는" 강제 장치가 서 있다. 되돌리기(revert)로 안전망의 반대편 — 실수해도 복구된다는 안심 — 까지 확인한다.
인프라를 세울 때 코드를 GitHub에 올렸다. 그때 Git은 "이력이 남는 보관소"였다. 이 장에서는 Git을 작업의 방식으로 끌어올린다. 왜 하필 지금인가 — 테스트라는 안전망이 생겼기 때문이다. 안전망이 있으면 대담하게 바꿀 수 있고, 대담하게 바꾸려면 되돌릴 수 있다는 확신이 필요하다. Git이 그 확신을 준다.
이 "순서"에는 의도가 있다. Git을 첫날에 가르치지 않은 이유는, 되돌릴 대상(코드)도 없고 되돌려야 할 이유(대담한 변경)도 아직 없던 시점에 명령어부터 외우게 하면 Git이 그저 귀찮은 절차로만 각인되기 때문이다. 지금은 다르다. 테스트가 있고, AI가 여러 파일을 한꺼번에 바꾸는 경험을 이미 했다. "이걸 잘못 건드리면 어쩌지"라는 불안을 한 번쯤 느껴 본 지금이, Git이 왜 필요한지 몸으로 이해되는 타이밍이다. 도구는 필요를 느낀 다음에 배워야 손에 붙는다.
앞에서 만든 자동화된 테스트를 흔히 '안전망'이라 부른다. 곡예사가 높은 줄 위에서 과감한 동작을 시도할 수 있는 것은, 떨어져도 받쳐 줄 그물이 아래에 있기 때문이다. 그런데 그물에는 사실 두 겹이 있다. 하나는 떨어지는 것을 알아채는 그물(테스트 — "지금 뭔가 망가졌다"를 알려 준다), 다른 하나는 떨어진 뒤 원래 자리로 되돌려 주는 그물(Git — "망가지기 전으로 돌아간다"를 가능하게 한다). 테스트만 있고 되돌리기가 없으면, 망가진 것을 알아도 손으로 하나하나 원상 복구해야 한다. 그건 안전망의 절반만 가진 것이다. 이 장은 그 나머지 절반, 되돌리는 능력을 세운다.
정리하면 이렇다. 안전망은 자신감을 주고, 되돌리기는 그 자신감을 행동으로 바꾼다. "고쳐도 된다"는 확신이 있어야 사람은(그리고 AI는) 낡은 코드를 과감하게 뜯어고친다. 되돌릴 수 없다는 두려움은 코드를 조심스럽게, 그래서 개선되지 않은 채로 방치하게 만든다. Git은 그 두려움을 걷어 내는 도구다.
Git은 코드의 시간여행 장치다. 모든 변경을 지점(커밋)으로 저장해 두므로, 언제든 과거의 어느 지점으로 돌아갈 수 있다. 이 능력이 개발의 심리를 바꾼다 — "망가뜨리면 어쩌지"가 "망가뜨려도 돌아오면 돼"가 된다. AI에게 과감한 변경을 맡길 수 있는 것도 이 안전장치 덕분이다.
'Git'이라는 이름 자체에는 거창한 뜻이 없다. 리눅스를 만든 리누스 토르발스가 2005년에 이 도구를 만들면서 붙인 이름인데, 본인이 농담처럼 "고집 센 놈"이라는 영국식 속어에서 따왔다고 밝혔다. 이름의 유래보다 중요한 것은 Git이 오늘날 전 세계 개발의 사실상 표준이라는 사실이다. 회사에 들어가든, 오픈소스에 기여하든, 혼자 만들든 — 코드를 다루는 거의 모든 현장이 Git으로 돌아간다. 그래서 Git은 "이 강의에서만 쓰는 도구"가 아니라, 개발자로서 평생 쓰는 기본기다.
한 가지 흔한 오해를 미리 풀자. Git과 GitHub는 다른 것이다. Git은 내 컴퓨터에서 변경 이력을 관리하는 프로그램(시간여행 장치 본체)이고, GitHub는 그 이력을 인터넷에 올려 두고 남과 공유하는 웹 서비스(장치를 보관하는 클라우드 창고)다. Git 없이는 GitHub를 제대로 쓸 수 없지만, GitHub 없이도 Git은 내 컴퓨터 안에서 완전히 동작한다. 저장소를 처음 올릴 때 우리가 쓴 것은 주로 GitHub(창고에 올리기)였고, 이 장에서 파고드는 것은 Git 본체(이력을 다루는 법)다.
AI는 한 번에 여러 파일을 빠르게 바꾼다. 편리하지만, 그중 하나가 잘못됐을 때 "뭐가 바뀌었더라"를 사람이 다 기억할 수 없다. Git은 바뀐 것을 전부, 정확히 기록한다. AI의 작업을 커밋 단위로 끊어 두면, 문제가 생겼을 때 "이 커밋부터 이상하다"를 짚어 그 지점만 되돌릴 수 있다. Git은 AI 협업의 실행취소(undo)다.
이 점을 조금 더 풀어 보자. 사람이 직접 코딩하던 시절에는 변경이 느렸다. 한 시간에 파일 하나를 손보는 식이라, 무엇을 바꿨는지 머릿속에 대충 남아 있었다. AI는 다르다. 한 번의 지시로 데이터베이스 스키마, 백엔드 API, 프론트 화면을 동시에 건드리기도 한다. 속도가 빨라진 만큼 사람의 기억이 따라가지 못하는 구간이 생긴다. 이 간극을 메우는 것이 Git의 이력이다. "AI가 방금 무엇을 어떻게 바꿨는가"를 사람의 기억이 아니라 Git의 기록에 맡기는 것 — 이것이 AI 시대에 Git이 선택이 아니라 필수가 된 이유다.
또 하나. AI에게 일을 맡길수록 잘게 커밋하는 습관이 중요해진다. AI가 20개 파일을 한 번에 바꾸고 그걸 커밋 하나로 뭉쳐 두면, 나중에 그중 하나가 문제여도 20개를 통째로 되돌려야 한다. 반대로 의미 단위로 커밋을 나눠 두면, 문제가 된 그 한 덩어리만 도려낼 수 있다. AI의 작업 속도가 빠를수록, 그 결과물을 되돌릴 수 있는 '단위'를 사람이 관리해 줘야 한다. 이 이야기는 바로 뒤 용어 정리의 커밋 항목과 '프롬프트 작성법'의 커밋 단위 지시에서 이어진다.
[짚고 가기] "Git은 어렵다"는 선입견을 갖기 쉽다. 하지만 실제로 어려운 것은 Git의 내부 개념(브랜치·머지의 원리)이 아니라 명령어 암기인데, 이 과정에서는 그 명령어를 AI에게 시키므로 그 부담이 없다. 여기서 기억할 점은 이것이다 — Git 명령어를 외울 필요가 없다. 대신 Git으로 무엇을 왜 하는지만 이해하면 된다. 이렇게 마음먹으면 심리적 저항이 크게 준다.
git rebase같은 겁나는 명령을 외우던 예전 방식과는 다르다. 개념만 이해하면, 실행은 "커밋 나눠서 해줘" 같은 우리말 지시로 끝난다.
이 장에서 처음으로 제대로 다루는 Git 용어들이다. 앞에서 스치듯 나온 것도 있지만, 여기서 각각을 정확히 잡고 간다. 개발이 처음이라면 이 절이 이 장에서 가장 낯설 수 있는데, 완벽히 외울 필요는 없다. "이런 개념이 있구나" 정도로 눈에 익혀 두면 실습하며 자연스럽게 손에 붙는다.
변경을 저장하는 하나의 지점. 사진을 찍듯 그 순간의 코드 상태를 그대로 저장한다. 좋은 커밋은 "의미 있는 한 덩어리"다 — "결제 승인 로직 추가"처럼 한 가지 일을 담는다. 뒤죽박죽 열 가지를 한 커밋에 담으면, 나중에 그중 하나만 되돌릴 수 없다.
'커밋(commit)'이라는 단어는 원래 '맡기다·약속하다·확정하다'라는 뜻이다. 은행에서 "이 거래를 확정합니다"라고 도장을 찍는 순간을 떠올리면 감이 온다. 코드를 이리저리 바꾸다가 "여기까지를 하나의 완결된 상태로 확정한다"고 선언하는 것이 커밋이다. 확정하기 전까지의 변경은 아직 물 위에 떠 있는 임시 상태이고, 커밋하는 순간 Git의 이력에 영구히 새겨진다. 그래서 커밋은 되돌아올 수 있는 안전한 발판을 하나 만드는 행위이기도 하다. 발판이 촘촘할수록 넘어져도 멀리 떨어지지 않는다.
커밋 메시지는 왜/무엇을 바꿨는지를 남기는 기록이다. 이것도 AI가 잘 쓴다 — 바뀐 내용을 읽고 요약해 주므로. 다만 사람은 그 메시지가 실제 변경과 맞는지 본다.
커밋 메시지가 왜 중요한지 한 번 더 짚자. 3개월 뒤의 나(또는 동료)가 이력을
훑어보며 "이 코드는 왜 이렇게 됐지?"를 물을 때, 유일한 대답이 커밋 메시지다.
update, fix, 수정 같은 메시지는 아무 대답도 못 한다. 반대로 "골목 라이브
초과 판매 사고 재발 방지 — 결제 전 잔여석 재확인 추가" 같은 메시지는, 코드를
읽지 않고도 그 변경의 의도를 알려 준다. AI에게 커밋 메시지를 맡길 때 "무엇을
왜 바꿨는지 한국어로"라고 요구하는 이유가 여기 있다.
입문자가 커밋을 오해하는 흔한 방식이 있다. "커밋은 바뀐 부분만 저장하는 것"이라는 생각이다. 실제로 Git은 커밋마다 그 순간 프로젝트 전체의 상태(스냅샷)를 기억한다고 이해하는 편이 훨씬 정확하다. 그래서 어느 커밋으로 돌아가든, 그 시점의 프로젝트가 통째로 재현된다. 되돌리기가 든든하게 느껴지는 이유가 이것이다 — 과거의 한 장면을 부분이 아니라 통째로 붙잡아 두기 때문이다. (내부적으로 Git이 저장 공간을 아끼는 영리한 방법을 쓰긴 하지만, 사용자 입장에서는 "커밋 = 그 순간의 전체 사진"으로 이해하면 충분하다.)
본류에서 갈라져 나온 작업 가지. 여러 사람이(또는 여러 실험이) 서로를 방해하지 않고 동시에 일하게 해 준다. 온티켓처럼 혼자 하는 프로젝트에서도 브랜치는 유용하다 — "결제 기능"을 별도 가지에서 만들고, 다 되면 본류에 합친다. 만드는 도중의 불완전한 코드가 본류(언제나 돌아가야 하는 곳)를 더럽히지 않는다.
feature/payment 처럼 작업별로 갈라 만든다'브랜치(branch)'는 그대로 '나뭇가지'라는 뜻이다. 나무의 굵은 줄기(main)에서 가지가 뻗어 나오고, 그 가지 끝에서 새 잎(변경)이 자란다. 가지에서 아무리 실험을 해도 줄기는 멀쩡하다. 그러다 그 가지의 열매가 쓸 만하면 줄기에 접붙이고 (머지), 실패한 실험이면 가지째 잘라 버리면(브랜치 삭제) 그만이다. 이 비유가 브랜치의 핵심을 담는다 — 위험한 시도를 본류와 격리하는 것.
왜 굳이 가지를 나누는지 구체적인 장면으로 보자. 온티켓은 지금 옆자리 동료가
접속해 티켓을 예매할 수 있는, '돌아가는' 상태다. 그런데 결제 화면 문구를 손보다가
실수로 예매가 안 되게 만들었다고 하자. 이 변경이 곧장 main에 들어가 있으면,
그 순간 온티켓 전체가 멈춘다. 반면 feature/payment-polish라는 가지에서
작업했다면, main의 온티켓은 여전히 멀쩡히 돌아가고, 내 실수는 그 가지 안에만
갇혀 있다. main은 언제나 초록불이라는 원칙 — 이것이 브랜치를 나누는 진짜 이유다.
여기서 두 가지 관련 용어를 함께 익혀 두자. 지금 내가 서 있는 브랜치를 체크아웃(checkout)했다고 말한다. 작업 공간을 그 가지의 상태로 바꿔 놓는 것이다. 그리고 여러 브랜치가 갈라졌다 합쳐지는 그림 전체를 흔히 브랜치 트리라고 부른다. 이 과정에서는 이 명령들을 직접 칠 일이 거의 없다 — "이 작업은 새 브랜치에서 해줘" 같은 지시로 AI가 대신 갈아타 준다.
머지는 브랜치의 변경을 다른 브랜치로 합치는 것이다. PR(풀 리퀘스트) 은 "이 브랜치를 main에 합쳐도 될까요?"라고 정식으로 요청하는 절차다. 회사에서 PR은 검토와 자동 검사의 관문 역할을 한다 — 사람이 코드를 리뷰하고, 자동 테스트가 통과해야 합쳐진다. 우리 팀에서도 PR을 검증 직군과 자동 검사가 만나는 지점으로 쓴다.
'머지(merge)'는 '합치다·병합하다'라는 뜻 그대로다. 나뭇가지 비유로 돌아오면, 따로 자란 가지의 열매를 줄기에 접붙이는 순간이 머지다. 대개는 Git이 알아서 두 갈래의 변경을 매끄럽게 합쳐 준다. 다만 같은 파일의 같은 줄을 양쪽에서 서로 다르게 바꿨을 때는 Git이 어느 쪽이 옳은지 판단할 수 없어서, 사람에게 "이 부분은 둘 중 뭘로 할래?"라고 묻는다. 이것이 흔히 말하는 충돌(conflict)이다. 혼자 하는 온티켓 프로젝트에서는 충돌이 드물지만, 개념만 알아 두자 — 충돌은 사고가 아니라 "사람의 결정이 필요한 지점"이라는 Git의 정중한 물음이다.
'PR'은 왜 하필 '풀 리퀘스트(pull request)', 곧 '당겨 가 달라는 요청'일까. 내가 만든 가지의 변경을 상대(main을 관리하는 쪽)에게 "당신 쪽으로 당겨(pull) 가져가 합쳐 주세요"라고 부탁하는 데서 나온 이름이다. 핵심은 머지가 자동이 아니라 요청이라는 점이다. 곧장 합치지 않고 일부러 한 박자 멈추는 것 — 그 멈춤 동안 사람이 리뷰하고 자동 검사가 돌아간다. 즉 PR은 "합치기 전에 반드시 거쳐야 하는 관문"을 만드는 장치다. 과정 첫머리부터 이어진 퍼미션(사람이 검토하고 승인하는 절차)의 정신이, Git의 세계에서는 PR이라는 형태로 나타난다고 봐도 좋다.
PR 화면이 실제로 보여 주는 것은 무엇이 바뀌었는가(diff)다. 어떤 파일의 어느 줄이 지워지고 어느 줄이 새로 들어왔는지가 색으로 표시된다. 코드 전체를 다시 읽는 게 아니라 '바뀐 부분만' 집중해서 검토할 수 있게 해 주는 것 — 이것이 PR이 검토의 장소가 되는 이유다. 뒤의 브랜치 흐름 실습에서 이 화면을 직접 연다.
스테이징은 "이번 커밋에 포함할 변경"을 골라 담는 단계다(git add).
.gitignore는 "버전 관리에서 제외할 것" 목록 — .env, node_modules 등.
저장소를 GitHub에 처음 올릴 때 이미 이 제외가 우리를 지켰다. 여기서 다시 강조하는 이유: 커밋 훅으로
이 규칙을 한 번 더 강제할 것이기 때문이다.
'스테이징(staging)'을 조금 더 풀자. 'stage'는 무대·연단이라는 뜻인데, 여기서는 "커밋이라는 무대에 올릴 배우를 고르는 대기 공간"이라고 보면 된다. 파일을 이것저것 바꿔 놓았어도, 그중 이번 커밋에 넣을 것만 무대에 올린다(스테이징한다). 나머지는 무대 뒤에 남겨 두고 다음 커밋으로 미룰 수 있다. 이 '고르기' 단계가 있어서, 앞서 말한 의미 단위 커밋이 가능해진다. 관련된 변경만 골라 담고, 성격이 다른 변경은 따로 담는 것이다. 우리는 이 고르기까지 대개 AI에게 맡긴다 — "테스트 추가는 테스트대로, 승인 로직 수정은 그것대로 나눠서 커밋해줘"처럼.
Git이 변경을 다루는 세 자리를 그림으로 정리하면 이렇다.
.gitignore도 다시 짚자. 'ignore'는 '무시하다'라는 뜻으로, 이 파일에 적힌
목록은 Git이 아예 못 본 척한다. 왜 어떤 파일은 일부러 이력에서 빼는가 —
크게 두 부류다. 첫째, 비밀(시크릿). .env에는 결제 키나 데이터베이스 접속
정보 같은 민감한 값이 들어 있다. 이게 GitHub에 올라가면 남이 우리 계정을 쓸 수
있다. 둘째, 자동 생성물. node_modules(내려받은 부품 더미)나 빌드 결과물은
언제든 다시 만들 수 있으므로 이력에 담을 이유가 없고, 담으면 저장소만 무거워진다.
.gitignore는 "이건 애초에 커밋 대상이 아니다"를 선언해, 실수로 딸려 들어가는
것을 1차로 막는다. 그리고 이 장에서 커밋 훅이 그 위에 2차 방어를 얹는다.
지금까지의 훅은 Claude Code의 훅(.claude/)이었다. 이 장에는 다른 종류가
등장한다 — Git 훅. Git이 커밋·푸시 같은 순간에 스스로 실행하는 검사다.
대표적으로 pre-commit 훅은 커밋이 이루어지기 직전에 끼어들어,
정해진 검사(테스트 실행, 시크릿 검사 등)를 하고 실패하면 커밋을 거부한다.
'훅(hook)'은 '갈고리'라는 뜻이다. 프로그램이 진행되는 흐름의 특정 지점에 갈고리를 걸어 두고, 그 지점에 이르면 내가 정한 동작을 자동으로 낚아채 실행하게 하는 장치를 통틀어 훅이라 부른다. 'pre-commit'의 'pre-'는 '~하기 전'이라는 접두사이니, pre-commit 훅은 곧 '커밋하기 직전에 걸린 갈고리'다. 커밋 명령이 떨어지면, 실제로 커밋이 새겨지기 전에 이 갈고리가 먼저 낚아채서 검사를 돌린다. 검사를 통과하면 손을 놓아 커밋을 진행시키고, 실패하면 붙잡아 커밋을 막는다.
Git에는 pre-commit 말고도 여러 시점의 훅이 있다(커밋 메시지를 검사하는
commit-msg, 원격에 올리기 직전의 pre-push 등). 원리는 모두 같다 — 특정
순간에 자동으로 끼어드는 검사. 이 장에서는 가장 많이 쓰이고 효과가 즉각적인
pre-commit에 집중한다.
두 훅의 관계를 정리하자. Claude Code 훅은 AI의 행동을 막고, Git 훅은 커밋 자체를 막는다. Git 훅은 사람이 직접 커밋해도 작동하므로, 방어선이 AI 밖으로까지 확장된다. 겹겹의 방어라는 원칙이 도구를 갈아타며 이어진다.
이 차이가 왜 중요한지 표로 또렷이 해 두자.
| Claude Code 훅 | Git 훅(pre-commit) | |
|---|---|---|
| 어디에 사는가 | .claude/ 설정 |
.husky/ 또는 Git 설정 |
| 언제 작동하나 | AI가 도구를 쓰려는 순간 | 커밋이 이루어지기 직전 |
| 누구를 막나 | AI의 행동 (파일 수정·명령 실행 시도) | 커밋 그 자체 (누가 커밋하든) |
| 사람이 직접 하면 | 사람의 손 작업은 Claude Code 밖이라 미적용 | 사람이 손으로 커밋해도 작동 |
| 뚫렸을 때 | Git 훅이 뒤에서 받쳐 준다 | Claude Code 훅이 앞에서 이미 한 번 걸렀다 |
핵심은 적용 범위가 다르다는 것이다. Claude Code 훅은 AI가 우리 도구 안에서 움직일 때만 작동한다. 그런데 협업자가 늘거나, 급할 때 사람이 터미널에서 직접 커밋하거나, 다른 AI 도구를 쓰는 상황이 오면 Claude Code 훅의 우산 밖이다. Git 훅은 그 바깥까지 덮는다 — 누가, 어떤 도구로 커밋을 시도하든 Git이라는 공통의 길목을 반드시 지나기 때문이다. 그래서 두 훅은 경쟁이 아니라 보완이다. 안쪽(AI 행동)은 Claude Code 훅이, 바깥쪽(커밋 경로)은 Git 훅이 지킨다.
이 장의 마지막 실습에 나오는 용어를 미리 잡아 두자. revert는 '되돌리다·뒤집다'라는 뜻인데, Git에서의 revert는 초심자의 직관과 살짝 다르다. 많은 사람이 "되돌리기"를 "과거를 지우고 없던 일로 만드는 것"으로 상상한다. Git의 revert는 그렇지 않다. 과거의 어떤 커밋이 한 일을 정확히 반대로 실행하는 '새 커밋'을 하나 더 쌓는다.
비유하면 이렇다. 장부에 "100원 입금"이라고 잘못 적었을 때, revert는 그 줄을 지우개로 지우는 게 아니라 아래에 "100원 출금(위 입금 취소)"이라고 새로 한 줄을 적는 방식이다. 결과적으로 잔액은 원래대로 돌아오지만, "입금했다가 취소했다"는 사실 자체가 장부에 남는다. 이것이 왜 좋은가 — 이력을 조작하지 않기 때문에 안전하다. 누구도 과거를 몰래 바꾸지 않았고, 무슨 일이 있었는지 모두 투명하게 보인다. revert의 이 성질은 이 장 실습의 마지막 따라하기와 정리에서 다시 확인한다.
[짚고 가기] 용어가 한꺼번에 쏟아지지만, 지금 다 외우려 할 필요는 없다. 실전에서 정말 손에 붙어야 하는 감각은 세 가지로 압축된다 — (1) 커밋은 되돌아올 수 있는 발판, (2) 브랜치는 본류를 지키는 격리 공간, (3) 나머지 명령은 AI가 대신 쳐 준다. 나뭇가지 비유(브랜치·머지)와 장부 비유(revert)를 머릿속에 간단히 그려 두면 오래 남는다. 특히 Git과 GitHub는 헷갈리기 쉬운데, "Git은 내 컴퓨터의 시간여행 장치, GitHub는 그걸 올려 두는 클라우드 창고"라는 한 줄만 붙들면 구분이 또렷해진다.
앞에서 만든 테스트에는 약점이 하나 있다. 사람이 안 돌리면 그만이라는 것. 바빠서, 깜박해서, "이 정도야 괜찮겠지" 해서 — 테스트는 돌리지 않는 순간 없는 것과 같다. 테스트를 만들 때 말한 "문화가 아니라 구조"가 여기서 완성된다.
이 "문화가 아니라 구조"라는 말을 이 장의 핵심 문장으로 다시 새기자. 문화에 기대는 방식은 "우리 팀은 커밋 전에 꼭 테스트를 돌립시다"라고 약속하고, 각자의 성실함에 맡기는 것이다. 좋은 마음이지만 약하다. 사람은 바쁘면 잊고, 급하면 건너뛴다. 한 명만 어겨도 규칙은 무너진다. 구조에 기대는 방식은 다르다 — 애초에 테스트를 통과하지 못하면 커밋이 물리적으로 안 되게 만들어 둔다. 성실함을 요구하지 않는다. 잊어도 상관없다. 구조가 대신 기억해 주기 때문이다. 좋은 안전장치의 조건은 "사람의 의지에 의존하지 않는 것"이다. 커밋 훅이 바로 그렇다.
pre-commit 훅에 테스트를 걸면, 테스트를 통과하지 않은 코드는 커밋할 수 없다.
이제 "테스트 돌리는 걸 잊었다"가 불가능해진다. 커밋하려는 순간 자동으로 돌고, 빨간불이면 커밋이 거부된다. 열심히가 아니라 강제. 이것이 안전망을 '있는 것'에서 '작동하는 것'으로 바꾸는 마지막 단계다.
'있는 것'과 '작동하는 것'의 차이를 실감 나게 보자. 소화기가 사무실 구석에 있다고 불이 꺼지는 게 아니다. 불이 났을 때 누군가 그것을 집어 든 순간 비로소 작동한다. 테스트도 똑같다. 파일로 존재하기만 하는 테스트는 구석의 소화기다. 그런데 커밋 훅은 "커밋할 때마다 자동으로 소화기를 점검하고, 불씨가 있으면 문을 잠그는" 장치다. 사람이 소화기를 집어 들 것을 믿는 대신, 집어 드는 행위 자체를 구조에 심어 둔 것이다. 안전망은 이렇게 자동으로 당겨질 때에만 진짜 안전망이 된다.
여기에 시크릿 검사도 함께 건다 — 커밋에 .env나 키가 섞여 들어가려 하면
차단. 시크릿 파일을 막던 Claude Code 보안 훅이 AI를 막았다면, 이 Git 훅은 커밋 경로 전체를 막는다.
시크릿이 이력에 한 번 들어가면 왜 특별히 위험한지 알아 두면, 이 이중 방어의
가치가 와닿는다. 앞서 커밋은 그 순간의 전체 상태를 그대로 저장한다고 했다. 문제는
시크릿이 담긴 커밋을 나중에 지워도, 이력의 과거 어느 지점에는 그 시크릿이
그대로 남는다는 점이다. 특히 GitHub에 한 번 올라가면, 그 사이 누군가 봤을
가능성까지 감안해 결제 키·접속 정보를 전부 새로 발급해야 하는 사고가 된다.
그래서 시크릿은 "들어간 뒤 지우는" 게 아니라 "처음부터 못 들어가게 막는" 것이
정답이다. .gitignore가 1차로 막고, 커밋 훅이 2차로 막고, 보안 점검 때 세운 Claude Code
보안 훅이 AI 경로에서 또 막는다. 같은 사고를 세 겹으로 막는 셈이다.
[짚고 가기] "이렇게까지 막으면 오히려 불편하지 않나?"라는 생각이 들 수 있다. 여기서 짚어둘 점은 불편함의 위치가 다르다는 것이다. 커밋 훅이 없으면 당장은 편하지만, 사고가 났을 때의 불편(장애·시크릿 유출 수습)이 어마어마하다. 반대로 커밋 훅이 있으면 커밋할 때마다 몇 초를 쓰는 대신, 큰 사고를 원천 차단한다. "작고 예측 가능한 불편으로 크고 예측 불가능한 재앙을 막는다"는 안전 공학의 기본 거래인 셈이다. 실제로 공개 저장소에 키가 올라가 요금 폭탄을 맞은 기업 사고가 드물지 않다는 것을 떠올리면 경각심이 생긴다.
이 장의 지시들은 두 부류다. 하나는 강제 장치를 세우는 지시(커밋 훅 설치), 다른 하나는 일상의 작업 흐름을 도는 지시(브랜치·커밋·PR·revert)다. 앞 장들에서 익힌 원칙 — 목표를 말하고, 완료 기준을 넣고, 되돌릴 여지를 남긴다 — 이 여기서도 그대로 통한다. 특히 Git 작업은 "어떻게(명령어)"를 사람이 알 필요가 없는 대표적인 영역이라, "무엇을 왜"에만 집중한 지시가 잘 통한다.
Git pre-commit 훅을 설정해줘. 커밋 직전에:
1) api 테스트(pnpm test)를 실행해서 하나라도 실패하면 커밋을 거부
2) 커밋에 .env, .env.*, *.pem, test_sk_ 같은 시크릿이 포함되면 거부
거부될 때는 무엇 때문에 막혔는지 사람이 읽을 메시지를 보여줘.
(husky 같은 표준 도구를 써도 좋아. 설정 후 어떻게 동작하는지 설명해줘.)
이 지시가 왜 잘 짜였는지 뜯어 보자. 무엇을 검사할지(테스트·시크릿)를 목록으로 명확히 했고, 실패 시 어떻게 할지(거부)와 사람에게 어떻게 알릴지(읽을 메시지)까지 정했다. 마지막 괄호의 "설정 후 어떻게 동작하는지 설명해줘"가 특히 중요하다 — 훅은 눈에 안 보이는 곳에서 조용히 작동하므로, 설치 직후에 그 동작을 말로 설명받아 두어야 나중에 "왜 커밋이 막히지?"에 당황하지 않는다.
여기서 등장한 husky를 짚어 두자. husky는 Git 훅을 쉽게 설치·관리하게 해 주는 표준 도구다. Git 훅을 날것 그대로 설정하면 폴더 깊숙한 곳의 파일을 손봐야 하고 협업자와 공유하기도 번거로운데, husky는 그 훅 설정을 프로젝트 안에 담아 두어 누가 이 저장소를 받아도 같은 훅이 자동으로 걸리게 해 준다. 이름이 '허스키(썰매개)'인 이유는 특별한 뜻이라기보다 브랜드 네이밍에 가깝지만, "커밋마다 앞에서 든든히 지켜 주는 개"라고 기억하면 역할과 어울린다. 우리는 husky를 직접 설정하지 않는다 — 지시에 "husky 같은 표준 도구를 써도 좋다"고 열어 주면, AI가 알아서 설치하고 설정 파일까지 만들어 준다.
지시를 더 강하게 만드는 변형도 익혀 두면 좋다.
| 약한 지시 | 강한 지시 | 무엇을 더했나 |
|---|---|---|
| 커밋 전에 테스트 돌게 해줘 | (위 본문 지시 전체) | 무엇을·어떻게 거부할지·어떻게 알릴지를 모두 명시 |
| 시크릿 못 올리게 막아줘 | .env, .env.*, *.pem, test_sk_로 시작하는 값이 담기면 거부해줘 |
막을 대상을 구체적 패턴으로 지정 |
| 훅 설치해줘 | 설치 후 어떤 파일이 생겼고 언제 무엇을 검사하는지 설명해줘 | 보이지 않는 동작을 말로 드러내게 요구 |
지금부터 결제 관련 정리 작업을 feature/payment-polish 브랜치에서 할 거야.
브랜치를 만들고 그쪽으로 옮겨줘. 작업이 끝나면 의미 단위로 커밋을 나눠서
만들고, main으로 PR을 올려줘.
이 한 문장에 브랜치 흐름의 네 단계가 다 들어 있다 — 가지 만들기 → 그리로
옮기기(체크아웃) → 의미 단위 커밋 → main으로 PR. 사람은 이 흐름의 '이름'만
알면 되고, 각 단계의 명령어는 AI가 채운다. 브랜치 이름을 feature/payment-polish처럼
직접 지어 준 것도 요령이다. feature/라는 접두어는 "이건 기능 작업 가지"라는
분류를 한눈에 보여 주는 관행이고(그 밖에 버그 수정은 fix/를 쓰기도 한다),
뒤의 payment-polish는 이 가지가 무엇을 하는 가지인지 말해 준다. 이름만 봐도
용도를 알 수 있는 브랜치 이름은, 나중에 브랜치 트리가 복잡해졌을 때 큰 도움이 된다.
브랜치 흐름 지시의 다른 예시도 몇 개 익혀 두자.
지금 main에 있지? 마이티켓 조회 화면 개선을 새 브랜치에서 할 거야.
브랜치 이름은 feature/myticket-ui로 만들고 옮겨줘. 아직 커밋은 하지 마.
이 브랜치 작업 다 끝났어. 변경을 의미 단위로 커밋하고, main으로 PR을 올린 다음
PR 주소를 알려줘. PR 제목과 설명은 무엇을 왜 바꿨는지 한국어로 써줘.
두 번째 지시에서 "PR 제목과 설명"까지 챙긴 점을 보자. PR은 사람이 검토하는 장소이므로, 그 설명이 좋을수록 검토가 쉬워진다. AI에게 변경 요약을 맡기되 한국어로, 무엇을 왜라는 방향만 정해 주면 된다.
방금 바꾼 것들을 커밋해줘. 단, 한 커밋에 몰지 말고 의미 단위로 나눠:
테스트 추가는 테스트대로, 승인 로직 수정은 그것대로. 각 커밋 메시지는
무엇을 왜 바꿨는지 한국어로.
의미 단위 분리를 요구하는 이유 — 나중에 "승인 로직 수정만 되돌리고 싶다"가 가능해지기 때문이다. 뭉뚱그린 커밋은 되돌리기의 해상도를 떨어뜨린다.
'해상도'라는 말을 한 번 더 풀자. 사진의 해상도가 높으면 일부만 잘라내도 또렷하듯, 커밋을 잘게 나눠 두면 이력의 어느 한 조각만 정확히 도려낼 수 있다. 반대로 하루치 작업을 커밋 하나에 뭉쳐 두면, 그 안의 잘못된 한 줄 때문에 멀쩡한 나머지까지 통째로 되돌려야 한다. 그래서 "커밋을 나눠 달라"는 요구는 단순한 정리 취향이 아니라, 미래의 되돌리기를 위한 투자다. 이 원리는 뒤의 revert 실습에서 곧장 체감된다.
수정 지시의 예도 챙겨 두자. AI가 커밋을 원하는 만큼 잘게 나누지 않았을 때는 이렇게 되돌린다.
방금 커밋 하나에 테스트 추가랑 문구 수정이 같이 들어갔어.
그 커밋을 취소하고, 두 개의 커밋으로 다시 나눠줘.
커밋 메시지가 'update'처럼 성의가 없어. 각 커밋이 무엇을 왜 바꿨는지
알 수 있게 한국어로 다시 써줘.
[짚고 가기] 커밋 단위 지시는 "왜 굳이 나누지?"라는 의문이 가장 자주 드는 지점이다. 이건 말보다 뒤의 revert 실습과 연결해 보면 확 와닿는다. 커밋 세 개를 만들어 두고 "가운데 것만 revert"를 직접 해 보면, 나눠 둔 덕분에 정확히 그 하나만 도려내지는 장면을 볼 수 있다. 지금 나눠 두는 수고가, 나중에 사고 났을 때의 정밀 수술을 가능하게 한다 — 이렇게 정리하면 이유가 분명해진다.
이 실습의 골자는 앞 장들과 같은 정신이다 — 안전장치는 세우기만 하면 안 되고, 직접 당겨 봐야 믿을 수 있다. 먼저 훅을 세우고, 이어서 일부러 실패시켜 그 훅이 진짜 작동하는지 눈으로 확인한다. 그다음 브랜치 흐름을 한 바퀴 돌고, 마지막으로 되돌리기까지 체험한다.
아래 커밋 훅 지시를 실행한다. 설치가 끝나면 무엇이 생겼는지(대개
.husky/ 폴더나 Git 설정) 열어 보고, "언제 무엇을 검사하는지"만 읽어 낸다.
Git pre-commit 훅을 설정해줘. 커밋 직전에:
1) api 테스트(pnpm test)를 실행해서 하나라도 실패하면 커밋을 거부
2) 커밋에 .env, .env.*, *.pem, test_sk_ 같은 시크릿이 포함되면 거부
거부될 때는 무엇 때문에 막혔는지 사람이 읽을 메시지를 보여줘.
(husky 같은 표준 도구를 써도 좋아. 설정 후 어떻게 동작하는지 설명해줘.)
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
.husky/ 폴더를 열어 보면 대개 pre-commit이라는 파일이 있고, 그 안에
"커밋 직전에 이것들을 실행하라"는 짧은 스크립트가 담겨 있다. 한 줄 한 줄을
이해할 필요는 없다. 어느 순간(커밋 직전)에, 무엇을(테스트·시크릿 검사) 실행하는가
— 이 두 가지만 읽어 내면 충분하다. AI에게 "이 파일이 무슨 일을 하는지 한 줄씩
설명해줘"라고 물어 확인해도 좋다.
안전장치는 당겨 봐야 안전장치다(이제 익숙한 원칙이다).
지금 테스트를 일부러 하나 실패하게 만든 다음, 아무 변경이나 커밋을 시도해줘.
커밋이 거부되는지 보여주고, 거부되면 테스트를 원래대로 되돌린 뒤 다시 커밋해줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
커밋이 거부되는 장면을 눈으로 본다. 빨간불인 채로는 코드가 main 근처에도 못 간다 — 이 벽이 앞으로 온티켓을 지킨다.
거부되는 순간 터미널에 나타나는 메시지를 학생과 함께 읽자. 대략 "테스트가 실패해서 커밋을 중단했다"는 취지의 문구가 뜨고, 커밋이 만들어지지 않은 채 멈춘다. 여기서 확인할 것은 두 가지다 — (1) 방금 시도한 커밋이 이력에 남지 않았다는 것(거부는 아무 흔적도 남기지 않는다), (2) 테스트를 원래대로 고치자마자 같은 커밋이 통과한다는 것. 이 대비를 보면, 훅이 "될 때는 조용히 통과시키고 안 될 때만 막는" 문지기라는 감이 잡힌다.
api/.env 를 일부러 커밋에 포함시켜서 커밋을 시도해줘. 막히는지 확인만.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
.env가 커밋 문턱에서 튕겨 나오는 것을 확인한다. (시크릿 파일을 막던 Claude Code 훅과
이 Git 훅, 두 겹이 같은 사고를 막고 있다 — 하나가 뚫려도 다른 하나가 잡는다.)
여기서 학생이 흔히 헷갈리는 점 하나. .env는 원래 .gitignore에 들어 있어서
평소에는 커밋 대상에 아예 안 잡힌다. 이 실습은 그 .gitignore를 우회해 억지로
넣어 보는 것이라, "일부러 시크릿을 밀어 넣었을 때에도 커밋 훅이 최후에 잡아 주는가"를
확인하는 성격이다. 즉 1차 방어(.gitignore)가 뚫린 상황을 가정한 2차 방어 테스트다.
두 겹이 왜 필요한지 — 하나는 실수로 빠질 수 있어도 다른 하나가 받친다는 것 —
이 장면에서 확실히 각인된다.
아래 브랜치 지시를 실행해, 기능 브랜치에서 작은 작업(예: 결제 실패 메시지 문구 다듬기)을 하고 → 의미 단위로 커밋 → PR을 올린다. GitHub에서 PR 화면을 열어 본다 — 무엇이 바뀌었는지 한눈에 보이는 이 화면이, 검토가 이루어지는 곳이다.
지금부터 결제 관련 정리 작업을 feature/payment-polish 브랜치에서 할 거야.
브랜치를 만들고 그쪽으로 옮겨줘. 작업이 끝나면 의미 단위로 커밋을 나눠서
만들고, main으로 PR을 올려줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
PR 화면에서 특히 Files changed(바뀐 파일) 탭을 열어 보자. 초록색으로 표시된 줄이 새로 들어온 코드, 빨간색으로 표시된 줄이 지워진 코드다. 이 색칠된 차이(diff)를 훑는 것만으로 "이 PR이 무엇을 했는가"를 코드 전체를 읽지 않고 파악할 수 있다. 회사에서 리뷰어가 하는 일이 바로 이 화면을 보는 것이다. 혼자 하는 온티켓에서도 이 화면을 습관적으로 열어 두면, 내가 방금 무엇을 main에 합치려 하는지 한 번 더 검토하는 퍼미션의 순간이 된다.
브랜치 흐름을 한 바퀴 돌고 나면, 지금 온티켓의 이력이 어떤 모양인지 그려 보자.
main은 흔들림 없이 이어지고, 기능 작업은 옆 가지(D, E)에서 이뤄지다가
PR을 거쳐 M 지점에서 본류로 합쳐진다. 이 그림이 브랜치 흐름의 전부다.
시간여행을 직접 해 본다.
방금 만든 커밋 중 하나를 revert 해줘. 그리고 revert가 무엇을 되돌렸는지,
이력에는 어떻게 남는지 설명해줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
revert는 과거를 지우는 게 아니라 "이 변경을 취소하는 새 변경"을 쌓는다는 것을 확인한다. 되돌린 기록조차 남는 것 — 이것이 안심하고 실험할 수 있는 이유다. 망가뜨려도 돌아올 수 있고, 돌아온 것마저 기록된다.
이력이 어떻게 남는지 그림으로 확인하자. 커밋 E를 revert하면 이렇게 된다.
E는 그 자리에 그대로 있고, 그 뒤에 E'(E-프라임)라는 새 커밋이 하나 쌓였다.
결과물은 E 이전 상태로 돌아왔지만, "E를 했다가 E'로 되돌렸다"는 두 사건이
모두 이력에 남는다. 앞의 용어 정리에서 든 장부 비유 그대로다 — 잘못 적은 줄을 지우는
게 아니라, 취소하는 줄을 아래에 새로 적는다. 그래서 나중에 "아, E를 되돌린 게
실수였네, 다시 살리자"도 얼마든지 가능하다. 되돌리기를 되돌릴 수 있다는 것 —
이것이 revert가 안전한 이유이자, 과거를 지우는 방식(이력 조작)을 쓰지 않는 이유다.
[짚고 가기] revert를
reset이나checkout으로 "과거를 갈아엎는" 방식과 혼동하기 쉽다. 이 과정에서 다루는 것은 revert(새 커밋을 쌓아 되돌리는, 이력이 남는 안전한 방식) 하나뿐이라는 점을 분명히 해 두자. 과거를 지우는 방법도 있지만 그건 위험해서 쓰지 않는다 — 여기서의 되돌리기는 항상 기록을 남긴다. 안전을 최우선하는 이 과정의 일관된 태도다. 실습 후git log(또는 GitHub의 커밋 목록)를 열어 E와 E'가 나란히 남은 것을 직접 보면 "돌아온 것마저 기록된다"가 확실히 와닿는다.
.env가 커밋에 포함되려 할 때 거부되는 것을 확인했다