😱 Git Merge, 실수했다면? git revert로 안전하게 되돌리기

안녕하세요, 개발자 여러분!

팀 프로젝트에서 야심차게 개발한 기능을 main 브랜치에 머지(Merge)했는데... 앗! 😱 예상치 못한 치명적인 버그가 발견되어 긴급하게 배포를 되돌려야 하는 상황, 한 번쯤 겪어보셨을 겁니다.

이때 git reset으로 과거를 지워버리자니 이미 팀원들과 공유된 브랜치라 무섭고, 어떻게 해야 할지 막막하셨나요?

오늘은 이런 상황에서 우리의 히어로가 되어줄 git revert와, 특히 머지 커밋(Merge Commit)을 되돌릴 때 꼭 알아야 할 -m 옵션의 비밀에 대해 쉽고 자세하게 알아보겠습니다.

왜 머지 커밋 Revert는 특별할까요?

일반 커밋을 revert하는 것은 간단합니다. git revert <커밋_해시> 한 줄이면 끝이죠. Git이 해당 커밋의 변경 사항을 정확히 반대로 적용하는 새로운 커밋을 만들어주니까요.

하지만 머지 커밋은 부모(parent)가 두 개라는 점에서 다릅니다.

feature 브랜치를 main 브랜치에 머지한 상황을 생각해볼까요? 이 머지 커밋은 두 개의 부모를 가집니다.

  • 부모 1: 머지되기 전 main 브랜치의 마지막 커밋
  • 부모 2: 머지된 feature 브랜치의 마지막 커밋

이 때문에 git revert를 그냥 실행하면 Git은 혼란에 빠집니다. "두 부모 중 어떤 것을 기준으로 삼고, 어떤 부모로부터 온 변경 사항을 되돌려야 하지?" 라고 말이죠.

바로 이때, 우리가 Git에게 방향을 알려주는 나침반 역할을 하는 것이 -m (또는 --mainline) 옵션입니다.

실전! 머지 커밋 Revert 따라하기 (Step-by-Step)

자, 이제 feature/login 브랜치를 main에 머지했다가 되돌리는 시나리오로 직접 실습해보겠습니다.

STEP 1: 되돌릴 머지 커밋 찾기

가장 먼저, 문제의 머지 커밋을 찾아야 합니다. git log를 이용해 히스토리를 확인합시다.

unfold_lessbash
content_copyterminal
git log --oneline --graph

다음과 같은 로그를 확인했다고 가정해봅시다.

unfold_lessplaintext
content_copyaddcompare_arrowsopen_in_full
*   a1b2c3d (HEAD -> main) Merge branch 'feature/login'
|\
| * 7f8e9d0 (feature/login) Add login form validation
| * 6a5b4c3 Add login button
|/
*   d4e5f6g Initial commit on main

여기서 우리가 되돌리고 싶은 범인은 a1b2c3d 머지 커밋입니다.

STEP 2: 주축(Mainline) 부모 번호 확인하기

이제 -m 옵션에 쓸 부모 번호를 알아낼 차례입니다. git show 명령어로 머지 커밋의 상세 정보를 확인합니다.

unfold_lessbash
content_copyterminal
git show a1b2c3d

출력 결과 상단에 이런 정보가 보일 겁니다.

unfold_lessplaintext
content_copyaddcompare_arrowsopen_in_full
commit a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Merge: d4e5f6g 7f8e9d0
Author: Your Name <you@example.com>
Date:   ...

    Merge branch 'feature/login'

Merge: d4e5f6g 7f8e9d0 라인을 주목하세요!

  • d4e5f6g: 첫 번째 부모 (parent 1) 입니다. 즉, main 브랜치의 커밋입니다.
  • 7f8e9d0: 두 번째 부모 (parent 2) 입니다. feature/login 브랜치에서 온 커밋이죠.

우리는 feature/login에서 온 변경 사항을 없애고 싶으므로, main 브랜치의 히스토리를 주축(mainline)으로 유지해야 합니다. 따라서 부모 1번을 선택해야 합니다.

STEP 3: git revert 실행하기 ✅

이제 모든 준비가 끝났습니다. -m 1 옵션과 함께 revert 명령을 실행합니다.

unfold_lessbash
content_copyterminal
# git revert -m <주축_부모_번호> <되돌릴_머지_커밋>
git revert -m 1 a1b2c3d

이 명령을 실행하면, Git은 feature/login 브랜치를 통해 추가되었던 모든 변경 사항을 제거하는 새로운 커밋을 생성합니다. 커밋 메시지 편집기가 나타나면, 왜 되돌렸는지 명확한 이유를 적고 저장하면 작업 완료!

git log를 다시 확인해보면, Revert "Merge branch 'feature/login'" 이라는 새로운 커밋이 main 브랜치에 안전하게 추가된 것을 볼 수 있습니다.

⚠️ 가장 중요한 주의사항: 되돌린 브랜치, 다시 머지할 때

"좋아, 버그는 해결했고... 이제 feature/login 브랜치를 수정해서 다시 머지해야지!" 라고 생각하셨나요? 여기서 아주 중요한 함정이 있습니다.

문제점: 한 번 revert된 머지에 포함됐던 커밋들(6a5b4c3, 7f8e9d0)은 main 브랜치 입장에서 "이미 히스토리에 포함되었다가 되돌려진 내역"으로 기록됩니다. 그래서 나중에 수정된 feature/login 브랜치를 다시 머지하려고 하면, Git은 이전에 되돌렸던 커밋들의 변경 사항을 누락시킨 채 머지합니다.

해결책: "Revert를 Revert하라!" 이 문제를 해결하는 가장 깔끔한 방법은, 원래의 feature 브랜치를 다시 머지하기 전에, 이전에 했던 revert 커밋을 또다시 revert 해주는 것입니다.

  1. main 브랜치에서 feature/login 머지를 되돌린 revert 커밋을 찾습니다. (예: r1e2v3t)
  2. 이 revert 커밋을 다시 revert 합니다.
    unfold_lessbash
    content_copyterminal
    # Revert 커밋을 되돌려서, 과거의 feature/login 변경사항을 다시 복원
    git revert r1e2v3t
    
  3. 이제 main 브랜치는 feature/login의 변경 사항을 다시 받아들일 준비가 되었습니다.
  4. 수정된 feature/login 브랜치를 main에 머지합니다.
    unfold_lessbash
    content_copyterminal
    git merge feature/login
    
    이제 모든 변경 사항이 정상적으로 포함됩니다!

마치며: 오늘의 핵심 정리

git revert는 공유 브랜치의 히스토리를 깔끔하고 안전하게 관리할 수 있는 강력한 도구입니다. 머지 커밋을 되돌릴 때 오늘 배운 내용을 꼭 기억하세요!

  • 머지 커밋 Revert: git revert -m 1 <머지_커밋_해시>
  • 안전성: revert는 히스토리를 지우지 않고, 변경을 되돌리는 새로운 커밋을 생성합니다.
  • 재머지(Re-merge) 시: 반드시 "Revert 커밋을 다시 Revert" 하는 패턴을 기억하세요.

이제 실수로 잘못된 브랜치를 머지해도 당황하지 않고, 전문가처럼 우아하게 대처할 수 있겠죠?

Happy Coding! 🚀

오늘은 개발자들의 생산성을 확 높여주는 마법, CI/CD에 대해 이야기해보려고 합니다. 매번 코드를 수정하고, 빌드하고, 서버에 접속해서 파일을 올리는 반복 작업에 지치셨나요? CI/CD는 이런 과정을 자동화해서 우리가 더 중요한 일에 집중할 수 있게 도와줍니다.

 

🤔 CI/CD, 도대체 뭔가요?

CI/CD는 두 가지 개념의 조합입니다.

 

CI (Continuous Integration, 지속적 통합):

  • 핵심: 개발자들이 각자 작업한 코드를 자주 중앙 저장소(GitLab의 main, stage, prod 브랜치 같은 곳)에 합치는(merge) 것입니다.
  • 자동화: 코드가 합쳐질 때마다 자동으로 빌드되고 테스트됩니다.
  • 장점: 코드 충돌이나 숨어있는 버그를 조기에 발견하고 수정할 수 있어, 전체 프로젝트의 안정성이 높아집니다. "어? 내 컴퓨터에선 잘 됐는데!" 같은 상황이 줄어들죠.
  • YAML 파일에서: build-job, prod-build-job 부분이 바로 CI 단계입니다. 코드가 특정 브랜치로 푸시(push)되면, GitLab이 알아서 Node.js 환경을 만들고(image: node:18-alpine3.21), 필요한 라이브러리를 설치(npm install)한 뒤, 프론트엔드 코드를 웹 브라우저가 이해할 수 있는 파일들(HTML, CSS, JS)로 변환(npm run build...)합니다.

 

CD (Continuous Delivery/Deployment, 지속적 제공/배포):

  • 핵심: CI 단계를 성공적으로 통과한 결과물(빌드된 파일)을 자동으로 실제 사용자가 접속하는 서버 환경까지 전달하는 과정입니다.
  • 종류:
    • 지속적 제공(Delivery): 운영 서버 배포 직전까지만 자동화하고, 실제 배포는 사람이 버튼을 눌러 승인하는 방식입니다. (안정성이 중요할 때!)
    • 지속적 배포(Deployment): 모든 과정이 자동으로 진행되어, 코드가 합쳐지고 테스트를 통과하면 즉시 운영 서버에 반영되는 방식입니다. (빠른 업데이트가 중요할 때!)
  • YAML 파일에서: deploy-job, prod-deploy-job 부분이 CD 단계입니다. CI에서 만들어진 build 폴더 안의 파일들을 AWS S3라는 저장소로 복사(aws s3 sync)해서 웹사이트를 업데이트합니다.
    • prod-build-job과 prod-deploy-job에 when: manual 보이시죠? 이건 "운영(prod) 환경 빌드와 배포는 자동으로 하지 말고, 사람이 GitLab 화면에서 직접 실행 버튼을 눌러줘!"라는 뜻입니다. 즉, 지속적 제공(Delivery) 방식을 사용하고 있는 거죠.
    • main(개발)이나 stage(테스트) 브랜치는 when: manual이 없으니, 코드 변경 시 자동으로 빌드되고 배포될 가능성이 높습니다 (설정에 따라 **지속적 배포(Deployment)**에 가깝게 운영).

 

🧐 잠깐, image: docker:latest는 뭐죠?

파일 상단과 deploy-job 등에서 image: docker:latest를 사용하는 것을 볼 수 있습니다. 이는 해당 CI/CD 작업을 수행할 때 기본적으로 도커(Docker) 명령어를 사용할 수 있는 환경을 사용하겠다는 의미입니다. deploy-job에서는 이 도커 환경 안에서 apk add 명령어로 AWS CLI 도구를 설치하고 S3 및 CloudFront 명령어를 실행합니다.

 

 

✨ 캐시 무효화(Invalidation), 왜 필요할까요? (feat. CloudFront)

deploy-job과 prod-deploy-job을 보면 aws cloudfront create-invalidation이라는 명령어가 있습니다. 이게 바로 '무효화' 부분인데요, 왜 필요할까요?

  1. S3 + CloudFront = 빠른 웹사이트: 이 프로젝트는 빌드된 웹사이트 파일(HTML, CSS, JS 등)을 AWS S3라는 파일 저장소에 저장합니다. 그리고 AWS CloudFront라는 CDN(콘텐츠 전송 네트워크) 서비스를 이용해 사용자에게 파일을 더 빠르게 전달합니다.
  2. CDN의 똑똑한 캐싱: CloudFront는 전 세계 여러 지역에 있는 서버(엣지 로케이션)에 우리 웹사이트 파일의 복사본을 미리 저장(캐싱)해 둡니다. 사용자가 접속하면 가장 가까운 서버에서 파일을 받으니 로딩 속도가 빨라지는 원리죠.
  3. 캐시의 함정: 그런데 문제가 있습니다! S3에 새 버전의 파일을 업로드(배포)해도, CloudFront 서버에는 아직 이전 버전의 파일이 캐시되어 있을 수 있습니다. 그러면 사용자는 분명 배포가 끝났는데도 옛날 화면을 보게 되는 거죠. 😱
  4. 무효화 마법 주문!: aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*" 명령어는 CloudFront에게 "야, $DISTRIBUTION_ID 배포판에 캐시된 모든 파일(/*) 이제 유효하지 않아! 새로 가져가!" 라고 알려주는 역할을 합니다.
  5. 결과: 이 명령어를 실행하면, CloudFront는 캐시된 옛날 파일 대신 원본 저장소(S3)에서 최신 파일을 가져와 사용자에게 보여주고, 그 최신 파일을 다시 캐싱합니다. 덕분에 사용자는 배포 즉시 최신 웹사이트를 볼 수 있게 됩니다!
  6. "완료될 때까지 기다려!" 루프: while true; do ... done 부분은 CloudFront가 무효화 작업을 완료할 때까지 (상태가 Completed가 될 때까지) 10초마다 확인하는 코드입니다. 무효화는 즉시 끝나지 않기 때문에, 완료를 확인하고 파이프라인을 성공적으로 마무리하기 위한 장치입니다.


정리: 이 YAML 파일은 무슨 일을 하나요?

이 .gitlab-ci.yml 파일은 GitLab CI/CD를 통해 다음과 같은 멋진 자동화 흐름을 만듭니다.

  1. 개발/테스트/운영 브랜치(main, stage, prod)에 코드 변경이 감지되면:
    • (CI) 자동으로 해당 환경에 맞는 프론트엔드 코드를 빌드합니다. (단, prod는 수동 실행)
    • (CD) 빌드된 결과물을 각 환경의 AWS S3 버킷으로 업로드(배포)합니다. (단, prod는 수동 실행)
    • (CD - 무효화) AWS CloudFront 캐시를 무효화하여 사용자가 즉시 최신 버전을 볼 수 있도록 합니다.
  2. 안정성 확보: 운영(prod) 환경 배포는 수동으로 실행하도록 하여(Continuous Delivery), 실수로 잘못된 코드가 배포되는 것을 방지합니다.

이제 CI/CD가 어떻게 돌아가는지, 그리고 왜 캐시 무효화가 중요한지 감이 오시나요? 이 자동화 파이프라인 덕분에 개발팀은 코드 작성에 더 집중하고, 사용자는 더 빠르고 안정적으로 새로운 기능을 만날 수 있게 됩니다! ✨

프론트엔드 Github Flow 관리

Github Branch 용도

  1. 각 branch별 용도 설명
    1. release : 상용 배포용 브랜치. 사용시 release_frontend_20240813 등으로 별도 branch로 구분하여 사용
    2. develop : 개발 배포용 (현재 개인의 OCI) 브랜치
    3. feature : 로컬 작업용. 즉 로컬에서 작업하기 위한 환경 위주로 구성되어 있어야 함. 개인의 로컬 작업시에는 feature_dashboard , feature_publishing 등으로 별도 branch로 구분하여 사용
    4. 기타 feature_xxx : 로컬 작업이 끝난 개인의 feature branch

Git Action 관련 용어 설명

  1. 내려받기 Action
    1. fetch : 대상 git의 모든 branch의 Log와 상태를 내려받습니다. Log와 상태만 내려받을 뿐 실제로 소스가 병합되지는 않으니 안심하셔도 됩니다.
    2. pull : 대상 git의 대상 branch를 설정하여 소스의 변경사항을 처리합니다. 충돌나는 파일이 없다면 smooth하게 변경사항이 로컬 소스에 적용됩니다.
  2. 저장 및 올리기 Action
    1. commit : 현재 Local 소스를 자신의 feature branch에 저장합니다. Git의 장점으로, SVN에서는 오로지 서버 commit만 되었습니다. 때문에 서버가 날아가면 소스의 복구가 불가능했는데 Git을 사용함으로서 로컬이 괜찮다면 소스의 복구가 가능해진 면이 있습니다. 이렇게 상황에 맞춰 유동적으로 소스를 관리할 수 있어 Git의 사용이 일반화되었습니다.
    2. push : 현재 저장된 feature branch의 소스를 Git서버쪽으로 올립니다. 같은 이름의 branch로 올릴수도 있고 변경할 수도 있습니다.
  3. 병합과 충돌 Action
    1. merge : 여러 branch에 따로 떨어져 있는 소스를 병합합니다. 보통 master가 되는 branch를 따로 두고 master에 각 feature의 변경 사항이 반영되는 것으로 처리를 합니다. 그러므로 관리자가 필요하며 보통 전담할 수 있는 한 명의 개발자가 관리하는 것이 일반적입니다.
    2. conflict : 소스를 병합하는 위 merge 과정이나 소스를 내려받는 pull 과정에서 같은 소스에 서로의 차이점이 동시에 병합 처리되면 충돌이 발생합니다. 이를 conflict라 하며 처리하기 위해서는 작업자 각자의 Log를 재확인해야 하고 어느 부분에서 충돌이 났는지, 그리고 최선의 처리 방법은 무엇인지가 고민되어야 합니다. 보통 위 merge에서 설명한 바와 같이 단일 관리자로 관리합니다.
    3. 즉 merge와 conflict는 휴먼 에러의 종류로서, 처리해본 경험이 충분해야 처리할 수 있는 사안들로 팀 내에서 관리에 대한 협의가 잘 되어야 합니다.

Github Flow (Merge, Pull, Push 등)

  1. 개인의 로컬 작업이 완료되어 feature (이하 origin )에 병합이 필요할 경우
    1. 로컬 작업을 자신의 feature에 commit
    2. origin 과의 충돌 방지를 위해 fetch, pull - origin 에서 개인 feature branch로 향하도록 설정하시고 fetch, pull 을 실행하면 됩니다.
    3. Already Update (이미 업데이트 되었음) 이 뜨면 충돌이 날 염려가 없는 것이므로, 자신의 feature명과 같은 github branch에 push
    4. origin 과의 Merge(병합) 수행 - 작업 담당자 : 박정환 (고정)
    5. 병합 완료 후 전체 메세지로 완료 안내 및 pull 요청
    6. origin 병합 버전과 Sync하기 : 위의 b 작업을 한번 더 수행 (origin 과의 충돌 방지를 위한 fetch, pull 하면 됩니다.
    7. 로컬 정상 반영, 작업 지속
  2. conflict (충돌) 발생할 경우
    1. 바로 호출해 주시기 바랍니다. 충돌 난 파일은 보통 revert처리를 하거나, 작업자 별로 확인해서 최신의 Log를 확인해 덮어쓰는 것으로 해결할 수 있으나 우선은 전후상황 대처를 위해 확인이 필요합니다.
  3. 기타 평상시
    1. 출근 후 항상 origin 에 대해 개인 feature branch에 fetch, pull 을 실행하시는 것을 권장합니다.

Fetch와 Pull을 왜 같이 사용해야 하는가?

  • Fetch, Pull을 항상 세트로 사용하시는걸 권장드립니다. 이유는 아래와 같습니다.
    1. Fetch를 먼저 수행하면 리모트 저장소의 최신 변경 사항을 로컬로 가져오지만, 로컬 작업 브랜치에는 바로 적용되지 않습니다. 이렇게 하면 변경 사항을 미리 확인하고 분석할 수 있습니다. (Log를 가져오는 것이라고 생각하시면 됩니다.)
    2. Fetch를 통해 최신 상태를 확인한 후, 문제가 없다고 판단되면 그때 Pull을 수행합니다. 이 방식은 예상치 못한 충돌을 최소화하며, 작업의 안정성을 높입니다.
    3. Fetch 없이 바로 Pull을 하면 리모트 변경 사항이 즉시 로컬에 적용되기 때문에, 예상치 못한 충돌이 발생할 수 있습니다. 이러한 충돌은 작업 흐름을 방해할 수 있으며, 예기치 않은 Merge Log가 생성될 가능성도 있습니다.

git branch dev -- 내 우분투에서 브랜치 생성

git pull origin dev -- 깃허브 클라우드 dev를 내 우분투 안의 dev로 복사해 옴

git branch nuri --  우분투에서 내 브랜치 생성

git checkout nuri -- 우분투 브랜치를 nuri로 변경

git pull origin dev -- nuri 브랜치에  깃허브 클라우드 dev를 복사해 옴

git push origin nuri -- nuri 브랜치를 깃허브 클라우드에 올림 (nuri 클라우드가 생성 됌)

 

다운 후에는 npm i 한 번 해서 라이브러리 다운받기 

 

 

[업데이트 된 코드 반영할 때 (git merge 할 때)]

 

git pull -- 우분투 안의 모든 클라우드 업데이트

git merge origin/dev -- 업데이트한 dev 클라우드를 내 브랜치에 합침 (nuri인지 확인하기)

 

+ Recent posts