ASAC 07기 : 24.12.23 Git 개념 및 명령어와 브랜치 전략 및 배포 프로세스

2SEONGA·2025년 1월 7일
0

ASAC

목록 보기
6/13
post-thumbnail

Git CLI 사용을 위한 리눅스 Shell 명령어 학습

리눅스 쉘(shell, sh)

유저가 커널을 직접 다루기는 너무 어려워서 응용 소프트웨어로 간편한 제어 및 사용을 제공

리눅스 쉘 종류 : 과거 sh → 현재 bash 혹은 zsh

  • Bourne Shell (sh, 쉘 혹은 본쉘) : AT&T 벨 연구소의 스티븐 본 개발
    • 쉘을 수행할때 : #!/bin/sh = 상단에 셔뱅(shebang)을 추가한 뒤 스크립트를 작성(Batch File)
  • Bourne-Again Shell (bash, 바쉘 혹은 배쉬) : GNU 프로젝트 일환 개발, 유닉스 계열 운영체제 기본 쉘
    • 쉘을 시작할 때 = 터미널을 껐다키거나, source ~/.bashrc 통해 설정 재로딩 가능
      • 공통 : ~/**.bashrc** (rc : run commands)
      • 계정 : ~/**.bash_profile** (혹은 ~/.bash_login~/.profile 순서)
    • 쉘을 끝마칠 때 : ~/**.bash_logout**
    • 쉘을 수행할 때 : 스크립트 앞에 #!/bin/bash
  • Z Shell (zsh, 지쉘) : 커스텀이 자유로워 예쁜 테마를 가진 쉘을 사용할 수 있고, 인터렉티브가 훨씬 개선
    - 쉘을 시작할때 = 터미널을 껐다키거나, source ~/.bashrc 통해 설정 재로딩이 가능
    - 공통 : ~/**.zshrc** (rc : run commands)
    - 계정 : ~/**.zprofile** 또는 ~/**.zshenv**
    - (기타) 히스토리 저장 : ~/.zsh_history
    - (기타) 세션 관리 : ~/.zsh_sessions
    - 쉘을 수행할때 : 스크립트 앞에 #!/bin/zsh

리눅스 쉘(Bash/Zsh) 명령어 모음

디렉토리 및 파일 여행

  • pwd : 현재 디렉토리의 절대경로
  • ls : 디렉토리 내 어떤 파일들이 있는지 조회 | ls **-la** 혹은 ll 사용 (a 모든걸, l 상세하게)
  • cd : 디렉토리 이동 (들어가거나, 나올때) | 이전 디렉토리 cd **..** 상대경로 cd **./x** 절대경로 cd **/x**

디렉토리 및 파일 조작 (생성, 이동, 삭제)

  • mkdir : 새 디렉토리 생성
  • rm : 디렉토리 혹은 파일 삭제 | rm **-rf** (r 리컬시브, 내부에 있는 디렉토리들 모두, f 강제로 삭제)
  • cp : 디렉토리 혹은 파일 복사 | cp 혹은 cp -r (r 리컬시브, 내부에 있는 디렉토리들 모두)
  • mv : 디렉토리 혹은 파일 이동 혹은 명칭 변경
  • touch : 아무것도 없는 빈 파일 생성

파일 출력

  • cat : 파일 출력
  • echo : 문자열 출력, 일반적으로 환경변수 값을 확인하는데에 사용 | echo $ZSH_CUSTOM
    • 단발적 환경변수 설정 : export ZSH_CUSTOM="Hello, World" (영구 설정은 .zshrc 내 할 것)
  • head : 파일 첫 라인 출력 | head -n 10 제외 가능 head -10
  • tail : 파일 끝 라인 출력 | tail -n 10 혹은 실시간 로그 조회 시 tail -f -n 10 (10번째부터)
    • Apache 웹서버 로그 : tail -f /var/log/apache2/access.log
    • Nginx 웹서버 로그 : tail -f /var/log/nginx/access.log
      • 맥북 Nginx 로그 조회 실습 : tail -f /usr/local/var/log/nginx/access.log
    • Tomcat 웹서버 로그 : tail -f /var/log/tomcat/access.log / catalina.out

명령어 확장 : | (파이프 Pipe) = 연결되어 이어 수행

  • 프로세스 꼬리물기 : 프로세스들은 exec 라는 시스템 콜 함수를 통해 fork가 발생

  • $ls | sort | less = ls 명령어 수행 후 그 결과 기반으로 → sort 명령어 수행 후 그 결과 기반으로 → less 명령어 수행

  • $ls -l | grep key | less = 또 다른 예시

  • grep : 문자열 중 특정 정규표현식 조건 검색 | 위에서 tail 통한 실시간 로그 조회 중 원하는것만 조회 시

    • tail -f /usr/local/var/log/nginx/access.log | grep favicon.ico
    • tail -f /usr/local/var/log/nginx/access.log | grep **-E** "127.0.0.1" (정규표현식)
    • tail -f /usr/local/var/log/nginx/access.log | **egrep** "127.0.0.1" (정규표현식)
      • -E : 정규표현식 사용을 위해서는
      • -i : 대소문자 구분없는 검색을 위해서는
      • -n : 검색된 라인 넘버 표기
    • 개발 회사에서 Tomcat 서버에 문제가 생겼을때 로그 내 특정 ID 로 검색하면
      • 몇 시, 몇 분, 몇 초에 에러가 발생했는지, 에러가 왜 발생했는지 추적이 가능
      • -A 10 혹은 -B 10, -C 10 : 해당 검색된 라인의 앞과 뒤에 50라인씩 추가로 출력해보는 것 추천

명령어 확장 : Override > or <, Append >>

  • pbcopy : 클립보드에 복사하기 (매번 마우스로 드래그 복사 X) | 윈도우에서는 clip 명령어를 사용
    • pbcopy < ~/.gitignore_global | 윈도우에서는 clip < ~/.gitignore_global
    • pbcopy < .ssh/id_rsa.pub | 윈도우에서는 clip < ~/.gitignore_global
    • cat ~/.gitignore_global | pbcopy | 윈도우에서는 cat ~/.gitignore_global | cliip
      • cat ~/.gitignore_global | pbcopypbcopy < ~/.gitignore_global 는 사실상 같은 역할을 하나, Override 를 잘못 사용했다가는 큰일 날 수 있기에 후자를 권장
  • echo 복습 : echo "Hello, World!" >> example.txt
  • cat 복습 : cat example.txt > copy.txt

기타 명령어 확장

  • && : 독립적으로 개별 수행 (| 파이프와 달리 앞 명령어 결과가 다음 명령어의 입력으로 들어가지 않음)
  • ; : 앞 명령어가 실패해도, 다음 명령어 실행 (&& 보다 완화된 연결)
  • & : 앞 명령어와 다음 명령어 동시실행
  • || : 앞 명령어가 실패하면, 다음 명령어 실행

유틸리티

  • chmod : 파일 권한 → 4 (READ) + 2 (WRITE) + 1 (EXECUTE) | 대상 : Owner + Group + Other
    • chmod **700** executable.sh = Owner 에게 4(READ) + 2(WRITE) + 1(EXECUTE) 권한 추가
    • chmod **+rwx** all-allow.sh = chmod a**+rwx** all-allow.sh = chmod **777** all-allow.sh
    • chmod **u+rwx** executable.sh = chmod **700** executable.sh
      • u : user
      • g : group
      • o : others
      • a : all
    • chmod **400** readable.sh
    • chmod **500** excutable.sh = Owner 에게 4(READ) + 1(EXECUTE) 권한 추가
  • tar : TAR 로 압축된 파일 풀기 | tar -xvf apache-tomcat-9.0.16.tar
    • -x : extract 압축풀기 ↔ -c : create 압축하기
    • -v : verbose 어떤 파일이 풀리는지 상세히보기
    • -f : filename 지정
  • sudo : superuser, 3개의 유저 (Root, User, Guest) 중 Root 권한 실행 | su root 권한 얻기, switch
  • vi : 텍스트 에디터 ↔ code : VSCode 에디터

로컬 코드 관리 : Git & 중앙 코드 및 협업 관리 : Github

로컬 코드 및 협업 시 문제 관리

협업 관리에 사용되는 툴로는 Github 뿐만아니라 Gitlab, Bitbucket, Gerrit 등이 존재

  • 로컬 코드 관리
    • 다양한 버전 : 하나의 코드에서 다양한 케이스의 구현을 개발/테스트 시
    • 히스토리 추적 : 작업 중 문제 발생했을때 이전/직전 버전으로 롤백
  • 중앙 코드 관리 (협업) : 하나의 프로젝트(파일)를 다수의 사람이 개발한다면?
    - 협업 관리 : 어? 저 그거 바꿨는데 → 어? 저도 그거 바꿨어요? → 네?… 말을 해주셨어야죠
    - 안전히 원격 저장 : 내 맥북이 도난 당한다면? → 맥북 실시간 위치 분석 결과 중국에 → 새 맥북에 PULL

로컬 코드 및 협업 관리 : Git 과 Github 등장

일반적인 버전관리와 Git 버전 관리 방식의 차이

  • 일반적인 버전 관리 : 파일 기반 → 중복된 내용이 계속 쌓여가기에 용량 비효율적
  • Git 버전 관리 : 변경사항(Diff) 기반 → 변경된 내용만 쌓여가기에 용량 효율적
    • Commit 의미 : 바로 직전 버전 기준 변경사항 저장 (히스토리 단위) - 아래 이미지에서 1mb 에 해당

중앙 원격 RemoteV3 까지 정보가 적재되어있지만, 타인 Local A(빨간색) + 타인 Local B(파란색)은 각자 버전의 V4 존재

  • 본 예시에서 타인 Local A(빨간색) 와 타인 Local B(파란색) 가 동시에 중앙 원격 Remote 에 푸시하려 한다면 **충돌 발생
  • Conflict 충돌 에 대해서는 한참 뒤인 5.장에서 설명할 예정**

Git (Local Repository) : 로컬 코드 관리 vs.

Github (Remote Repository) : 중앙 코드 관리 (협업)

  • Git
    • 다양한 버전 : 로컬에서 다양한 버전 작업
    • 히스토리 추적 : 이전 내용 확인
  • Github
    - 협업 관리 : 동료 개발자가 적용해놓은 최신 코드를 이어 받아 개발하기 + 내가 작업한 내용 동료들에게 공유
    - 안전히 원격 저장 : 로컬 작업 내용들 모두 원격으로 백업

Git 시작하기 2가지 방식 : Remote / Local 에서 시작

Git 설정이 정상적으로 된 경우 .git 디렉토리를 확인할 수 있다 → .git 을 어디서부터 생성하여 시작할까?
Local 인 Git 에서 먼저 .git 을 생성할지, Remote 인 Github 에서 먼저 .git 을 생성할지 결정이 필요하다

  • 초기 시작 : Local (내 컴퓨터) Remote (Github Repository) 에 Push = git push
  • 가져오기 : Remote (Github Repository) Local (내 컴퓨터) 로 Clone = git clone

  • Git 이 활성화되어 있는가? = .git 디렉토리가 생성되어 있는가?

  • .git 디렉토리 내 우리의 코드 작업을 추적하기위해 사용되는 모든 세부 디렉토리들이 존재한다.

⛔ 어? 저 Git 이 이상해요..

  • Spring 프로젝트가 아닌 Spring 프로젝트의 상위 디렉토리에 git init (.git 디렉토리 생성)
  • 프로젝트 내 이미 git init 으로 .git 디렉토리가 생겼는데, 프로젝트 안에서 또 git init >

Local(Git) 과 Remote(Github) 보안 통신은 SSH 사용 / 설정법

Github 은 Remote 와 Local 간 보안 통신 방법으로 2가지 방식을 제시한다 : HTTPS & SSH

  • HTTPS : 명시적으로 ID/PW 를 매번 작성해야하기에, 키로거 공격 위험 및 실제 사용 시 번거로움이 있다.
  • SSH : SSH 를 위한 비대칭키를 사용하여, 명시적으로 ID/PW 와 같은 키 입력이 없이 정말 간단히 통신 가능

SSH 을 위한 키 생성이 완료되면 ~/.ssh 디렉토리 안에 Private/Public Key 페어가 잘 생성된걸 볼 수 있다.

  • Github Settings 설정 → SSH and GPG keys 내 SSH Key 등록 (Authentication Key 타입)
    • 로컬 : 비공개키 (id_ed255519) — Github : 공개키 (id_ed255519.**pub**) ← 이걸 등록
    • 공개키 파일의 내용을 바로 클립보드에 복사하는 명령어 사용하여 바로 붙여넣기
      • 맥북 : cat id_ed255519.**pub** | **pbcopy**
      • 윈도우 : cat id_ed255519.**pub** | **clip**



Local 에서 시작하기 : 초기 시작 - 처음 프로젝트를 시작할때 방식

Local(Git) 내 Git 초기 설정 후 → Remote(Github) 내 새 Repository 에 Push

  1. Local(Git) 내 작업중인 프로젝트를 git init 으로 Git 초기 설정 후
    • git init : 현재 내 디렉토리를 Git 으로 관리하겠다는것을 선언 = .git 디렉토리 생성
  2. Remote(Github) 에 새 Repository 에 Push

실습 : 1. 로컬에서부터 Github 시작하기 - 명령어 호출 순서

  1. git init : 현재 작업중인 프로젝트를 Git 으로 관리하겠다는 선언
  2. git branch -M main : 디폴트 브랜치 명칭 변경 ← master 가 아닌 main 이라는 비차별 명칭
  3. git remote add origin git@github.com:aaron/example.git
    • 타겟 설정 : 업로드할 Remote, Github Repository 생성 후 지정

⚠️ README 파일 생성 금지! : 로컬의 파일이 원격으로 올라갈 때 충돌 발생 가능성

  1. git push -u origin main : Github Repository 타겟에 발사 = Push
    - git remote -v : 5번에서 앞서 추가한 원격 Github Repository 주소가 표기되는걸 확인

Remote 에서 시작하기 : 가져오기 - 협업, 현업에서 일반적 방식

Remote(Github)에서 새 Repository 생성 후 → Local(Git)에 Clone 으로 가져오기

  1. Remote(Github) 에 새 Repository 생성 후

    • 혹은 이미 누군가 자신의 Local 에서 git init 을 통해 Git 설정 뒤 Remote 에 업로드한 경우
      • 내 로컬에 그것을 가져와서 이어서 작업을 진행
  2. Local(Git)에서 git clone 을 통해 Remote(Github) 에 있는 소스 코드 가져오기

    실습 : 2. 원격에서부터 Github 시작하기 - 앞서 커밋 후 PUSH 한 결과물을 로컬로 가져와 진행

    • 원격 Github Repository 에서 프로젝트를 새로 생성하여 진행해도 무방

⚠️ README 파일 선택 : 단, 빈 Repository 를 Clone 하면 에러 발생

- warning: You appear to have cloned an empty repository.

  1. 앞서 로컬에서 시작하여 원격으로 업로드한 Github Repo 의 로컬 디렉토리 삭제 = `rm -rf`
  2. 앞서 로컬에서 시작하여 원격으로 업로드한 Github Repo 에서 로컬로 받아오기 = git **clone**
    • = git clone git@github.com:aaron/example.git
      • 현재 원격 프로젝트를 로컬에서 Git 으로 관리하겠다는 선언 = .git 디렉토리 생성
  3. 정상적으로 로컬에 설정된것을 확인할 수 있고, .git 디렉토리도 확인 가능 = git 활성화
  4. 이미 존재하는 README 파일에 그 안에 간단한 내용을 하나 더 추가하여 Commit
    • echo 'local-v2' > README.md : README 내 내용 추가 + git status : 확인
    • git commit -m "Local 2: second commit" : Commit 메세지 입력 (다른거 해도됨)
    • git log 혹은 git log --pretty=oneline : 방금한 Commit 을 확인
  5. git push -u origin main : 이미 앞서 Clone 을 통해 설정되어있는 타겟에 발사 = Push
  6. 원격 Github Repository 저장소 URL 접근 시 2개의 Commit 메세지와 내용이 보일 것

Git Remote 관리 및 Local 와의 동기화와 Conflict

Remote 주소 관리 및 Local/Remote Branch 관리

  • Git 에서 모든것은 크게 Local(로컬, Git) 과 Remote(원격, Github) 로 나눠 생각
    • Remote(Github) : 여러 사람들의 작업들이 모두 모여있고
    • Local(Git) : 내 작업이 모여있는데, Remote(Github) 에 있는 타 사람 작업의 참조(Ref)도 보유
      • Remote 주소 관리 : 코드를 어디에서 다운로드할지 (fetch) / 코드를 어디로 업로드할지 (push)
      • Branch 관리 : Local Branch CRUD / Remote Branch CRUD - 모든 브랜치 로컬서 관리 가능

  1. Remote 주소 관리

남의 코드어디서 다운로드할것인가, 나의 코드어디로 업로드할것인가

  • fetch : 어디서 다운로드할것인가 ← 원격에 올려놓은 남의 코드를 다운로드
  • push : 어디로 업로드할것인가 ← 나의 코드를 원격에 업로드하여 다른 개발자가 볼 수 있도록
  • (fetch) 주소 : 남의 코드를 가져올 곳 | (push) 주소 : 나의 코드를 업로드할 곳

  • Remote 관리 명령어 : CRUD 기준
    • Read 조회 : git remote -**v**-**v** = **v**erbose : 상세 출력 (verbose : 말많은, 상세한, 구구절절)
    • Create 추가 : git remote **add** **origin** git@github.com:aaron/example.git
      • origin : Alias 별명, 별칭 → 위의 스크린샷 예시에선 aaron 이라고 설정되어있음
      • git@github.com:aaron/example.git : 원격 주소 = Github Repository 주소
        • HTTPS 타입 : https://github.com/aaronryu/5th-git-from-local.git
        • SSH 타입 : git@github.com:aaronryu/5th-git-from-local.git
    • Update 갱신
      • 전체 변경 : git remote **set-url** **origin** git@github.com:aaron/example.git
      • 일부 변경 : git remote **set-url --push origin** git@github.com:aaron/example.git
    • Delete 삭제 : git remote **remove** **origin**
  • 어떤 경우에 Remote 를 여러개 사용하는가? : 특정 코드를 내 개인 Repository 에 백업하여 나중에 학습 시
  1. Local/Remote Branch 관리

로컬에서의 명령어를 통해 → 원격 Remote 브랜치 & 로컬 Local 브랜치 모두 관리

Git 에서는 기능별로 혹은 개발하는 개발자별로 다수의 Branch 로 구성 가능

  • Remote 에서 Branch 를 가져오는 방법 : git fetch -**p**-**p** = **p**rune
    • Remote 에서 삭제된 브랜치는 Local 브랜치에서도 지우기
  • Branch 관리 명령어 : CRUD 기준 (Update 제외)
    • Read 조회
      • git branch -**r**-**r** = **r**emote : 원격 Remote Branch 조회
      • git branch -**l**-**l** = **l**ocal : 로컬 Local Branch 조회
      • git branch -**a**-**a** = **a**ll : 원격 Remote + 로컬 Local Branch 모두 전체 조회
    • Create 추가 : git checkout **-b** **example-branch**-**b** = new-**b**ranch
      • 기준이 되는 브랜치에서 새로운 브랜치 생성이기에, 저 명령어를 어느 브랜치에서 입력하냐가 중요
    • Delete 삭제
      • 로컬 관리 : git branch --**delete**/-**D** **example-branch** : 로컬 Local Branch 삭제
        • --**delete** : 삭제 (현재 브랜치가 어디에도 머지되지 못했다면 경고와 함께 미삭제)
        • -**D** : 강제 삭제
      • 원격 관리 : git push --**delete** **origin** **example-branch** : 원격 Remote Branch 삭제
        • origin : 원격 주소(Github Repository 주소)에 대한 Alias 별명, 별칭
        • example-branch : 삭제하고자하는 브랜치명
  • 실습 : 원격에서 브랜치 생성 및 삭제를 하며, 로컬과 원격 브랜치들이 어떻게 바뀌는지 조회
    1. 앞서서 로컬에서 or 원격에서 Github 시작하기가 끝이 난 후 | git branch -a 통한 조회
    2. 원격 Github 에서 브랜치를 생성하고, 로컬에서 git fetch 수행 | git branch -l 통한 조회
      • 앞선 실습에서 로컬에서 Commit 총 2개를 했었는데, 이번엔 원격에서 Commit 1개 생성
        • 커밋 메세지는 "Remote 1: third commit" 으로 하여 원격에서한 커밋임을 명시
      • git branch -a : 원격에서 추가한 브랜치를 PULL 받기전엔 새 브랜치를 인지하지 못함
      • git fetch : 원격에 있는 최신 브랜치 정보들을 로컬 내 Remote 의 Reference 로 가져옴
      • git branch -a : 원격에서 추가된 새 브랜치가 로컬의 브랜치 정보에 모두 다 싱크
    3. 원격 Github 에서 브랜치를 삭제하고, 로컬에서 git fetch 수행 → 아무일도 일어나지 않음
      • git branch -a 조회 시, 원격에선 삭제된 브랜치이나 로컬에선 여전히 브랜치 레퍼 존재
    4. 다시, 로컬에서 git fetch -**p** 수행 시 모든걸 싱크 = -**p** = **p**rune 옵션이 갖는 의미 확인하기
      • 3번에서 삭제된 원격 Github 의 브랜치가 → 로컬 브랜치를 삭제하며, 삭제된 브랜치가 동기화 >
  • 실습 : 로컬에서 기존의 로컬 브랜치를 삭제하거나 기존의 원격 브랜치를 삭제
    1. 앞서 생성한 feature/1 브랜치 로컬에서 삭제 : git branch **--delete** feature/1
      • 새 커밋이 존재하나 어디도 머지되지 않았을때 삭제 거부 시 : git branch **-D** feature/1
    2. 앞서 생성한 feature/1 브랜치 원격에도 삭제 : git push **--delete** origin feature/1 >
  • 실습 : 로컬에서 새 브랜치를 생성하고 원격으로 푸시한 다음 기존의 브랜치와 머지 = 합침
    1. 2가지의 브랜치(main, sub)가 있는 상황에서 mainsub 에서 시작하는것의 차이
    - main : 최초 Commit 1개 ⇒ 여기서 시작할래?
    - sub : 최초 Commit 1개 + 추가 Commit 1개 ⇒ 여기서 시작할래?
    2. 두 브랜치 각자마다에서 새 브랜치 만들기 main, sub 후 그 차이를 원격/로컬에서 확인
    - advanced-main : 기존 최초 Commit 1개 + 새 Commit 1개
    - advanced-sub : 기존 최초 Commit 1개 + 추가 Commit 1개 + 새 Commit 1개
    - 주의 : 디렉토리 기반으로 히스토리를 적재하기에 브랜치명에 계층이 있어서는 안된다.
    - 예를 들어서 main/advanced d sub/advanced
    3. 앞서 만든 브랜치 중 advanced-main 브랜치 main 브랜치에 Pull Request 요청
    4. 앞서 만든 브랜치 중 advanced-sub 필요가 없어져서 삭제한 후 Push 를 통해 삭제 >

Local Branch 에 Remote Branch 동기화

1. Pull = Fetch + Merge : 원격 Github 에서 최신 브랜치 가져오기

  • Fetch : 원격 Github Repository 에서 생성/삭제된 브랜치를 가져오는것뿐만 아닌 최신 코드 모두 가져오기
    • 명령어 : git fetch -**p**-**p** = **p**rune
  • Merge : 로컬에 가져온 “원격 브랜치의 최신 코드” 를 “로컬 브랜치의 구식 코드” 에 합치는 작업
    • 다시 풀어 설명하면, 원격에 있던 branch 의 최신 코드를 로컬에 있던 branch 의 구식 코드에 합치기
    • 명령어 : git merge **FETCH_HEAD**

  • FETCH_HEAD : Local 로컬에서 Remote 원격으로부터의 최신 커밋을 가져왔을때 = 원격 최신 커밋
  • HEAD
    • Remote 원격에서 사용되는 경우 : remotes/origin/**HEAD** -> origin/**main** = 원격 기본 브랜치
      • 맨 처음에 Remote 원격으로부터 git clone 하여 가져왔을때 표기할 기본 브랜치 (예, main)
    • Local 로컬에서 사용되는 경우 : `HEAD -> main` = 현재 작업중인 브랜치에서의 가장 최근 커밋
  • ORIG_HEAD : (Local 로컬에서) 현재 HEAD 가 가르키는 커밋 바로 직전의 커밋 (이슈 대응을 위한 백업)
  • MERGE_HEAD : FETCH_HEAD + 로컬 HEAD, 이 두개를 합칠때 충돌 발생 시 충돌 해결 위한 머지 커밋

2. Local Branch 에 Remote Branch 동기화 시 Conflict 충돌 해결

  • Conflict 충돌이 발생한다는것은 어떤 의미인가?
    • 동료 개발자들이(Git, Local) 같은 파일에 각자 다른 작업을 수행하여 이 둘을 어떻게든 합쳐야한다는 뜻

  • 충돌 시나리오 1 : Pull 할때 충돌 Conflict 발생 (Git 이 알려줌) = V3 과 V3 모두 V2 라는 같은 기준 위에 작업 수행

  • 충돌 시나리오 2 : Push 받을때 충돌 Conflict 발생 (Github 이 알려줌) = V3 과 V3 모두 V2 라는 같은 기준 위에 작업 수행

  • 어떻게 해결할 수 있는가? 2가지의 Conflict 충돌 해결방법

    • Rebase : 두 작업을 먼저 상대의 작업을 존중하고, 그 위에 다시 내 커밋을 만들어 쌓아 합침
    • Merge : 두 작업을 유지한채 합쳐졌다는것을 의미하는 Merge 커밋을 추가하여 합침

  • 만약 로컬 Git 에 Conflict 충돌 해결방법을 설정하지 않았다면 아래 메세지가 뜨며 Pull 사용이 불가능
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 1), reused 4 (delta 1), pack-reused 0 (from 0)
Unpacking objects: 100% (4/4), 1.69 KiB | 432.00 KiB/s, done.
From github.com:6th-asac/html-and-js-review
 * branch            main  -> FETCH_HEAD
 * [new branch]      main  -> origin/main
hint: You have divergent branches and need to specify how to reconcil them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint: 
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint: 
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fetal: Need to specify how to reconcile divergent branches
  • 처음 Git 을 사용하거나 Git 버전이 바뀐 후 다시 사용 시, git pull 명령 시 위 메세지로 머지 설정 요구
hint:   git config pull.ff true       # merge (fast-forwarded if possible)
hint:   git config pull.ff false      # merge (never fast-fowarded)
hint:   git config pull.ff only       # fast-forward only
  • Git 버전 2.29 이상 버전부터는 위 옵션으로 대체되었다고 한다 : git -v

  • 머지 충돌이 발생할 경우의 수는 크게 2개로 나뉨 : (A) 같은 브랜치 원격과 로컬 충돌 + (B) 다른 브랜치 원격과 원격에서 충돌
  • 실습 : (A) 같은 브랜치 advanced-main 에서 [ 원격 브랜치 - 로컬 브랜치 ] 충돌
    • 앞서 오류없이 머지했던 advanced-main 브랜치에 대한 작업을 원격 - 로컬 양쪽에서 작업
    1. 원격에서 advanced-main 브랜치에 커밋 "Remote 2: fourth commit" 추가
    2. 로컬에서 advanced-main 브랜치에 커밋 "Local 3: fifth commit" 추가
    3. 원격 advanced-main 브랜치에 로컬 advanced-main 브랜치를 Push 시 머지 충돌 발생
    4. 어떻게 머지 충돌을 해결해야할까 : 나중에 할 수업이지만 당장은 Pull 받아 머지 커밋 생성 후 Push >
  • 실습 : (B) 다른 브랜치 advanced-main - main 간 = [ 원격 브랜치 - 원격 브랜치 ] 충돌
    1. 원격 main 브랜치에 커밋 "Remote 3: sixth commit" 추가
    2. (방금 작업했던) 원격 advanced-main 브랜치를 원격 main 브랜치에 Pull Request 발행
    3. 원격 advanced-main 브랜치를 원격 main 브랜치에 머지 시 충돌 발생
    4. 어떻게 머지 충돌을 해결해야할까 : 나중에 할 수업이지만 당장은 아래와 같이 해결
    • 로컬 advanced-main 브랜치에서 원격 main 브랜치를 Pull 받아 머지 커밋 생성 후 Push
    • 다시 원격 advanced-main 브랜치를 원격 main 브랜치에 머지 시 성공 >

Git Remote - Local 동기화 시 Conflict 충돌 해결 방법 2가지 : Rebase & Merge

Conflict 충돌이 발생한다는것은 어떤 의미인가?

  • 동료 개발자들이(Git, Local) 같은 파일에 각자 다른 작업을 수행하여 이 둘을 어떻게든 합쳐야한다는 뜻
  • Local 동기화 시 Conflict 충돌 해결 방법 2가지
    • Rebase : 현재 내 작업물의 (기준이 되는) (1) Base 를 다시 재설정한 뒤, (2) 다시 커밋을 생성
      • 단점 : 내가 작업했던 커밋들이 다시 생성되기에, 내가 열심히 했던 히스토리가 리셋
    • Merge : 현재 내 작업물과 Remote 에 업로드되어있는 상대 작업물 모두 존중하고, 머지 커밋을 생성
      • 단점 : 머지 커밋이 덕지덕지 생성되어서 머지 수가 많아짐에 따라 커밋 로그를 더럽힘

1. Conflict 충돌 해결책 2가지 : Rebase & Merge 원리

  1. Rebase 의미는 내가 작업한 V3 가 V2 를 기준으로 뒀었지만, 상대의 작업 V3 먼저 가져와 그걸 새 기준으로 내 작업을 다시
  • Rebase 는 상대를 존중하는 태도 = “내가 조금 고생하지 뭐”
  1. Merge 의미는 위 그림에서 내가 작업한 V3 과 상대의 작업 V3 모두 그대로 두고, 충돌나는것을 해결한 새로운 머지 커밋 추가
  • Merge 는 매우 간단하고 쉬운 솔루션이지만, 머지 커밋을 통해 커밋 로그들이 더러워짐 = “내가 편한게 최고지”

2. Conflict 충돌 해결책 2가지 : Rebase & Merge 단점

- Rebase 단점 : 내가 작업했던 커밋들이 다시 생성되기에, 히스토리가 리셋
- Merge 단점 : 머지 커밋이 덕지덕지 생성되어서 머지 수가 많아짐에 따라 커밋 로그들이 더러워짐

3. Rebase & Merge 복습

4. Conflict 충돌 해결 Rebase & Merge : 4개의 머지 전략

  • Remote 원격에서 가져온 FETCH_HEAD로컬 HEAD 를 Merge 하는데에 3가지 방식이 존재한다.
    • Fast-Forward : Local 에 어떠한 내 작업도 없을때 → Remote 에 타인의 작업 그대로 가져와 붙임
    • 3-Way Merge : Remote 에 있는 타인의 작업 그대로, 내 작업 그대로 보존하고새 머지 커밋 생성
    • Rebase : Remote 에 있는 타인의 작업 그대로, 그 위에 내 작업을 처음부터 다시 쌓기 = 커밋 재생성
    • Squash Merge : Remote 에 있는 타인의 작업들 모두 하나의 커밋으로 뭉쳐새 머지 커밋 생성

  • Rebase 와 Merge 는 별개의 것으로 비교되는데 Rebase 가 Merge 전략에 있는 이유는 Rebase 후 Merge 를 하기 때문
    (Fast-Forward Merge, Three-Way Merge, Squash and Merge, Rebase and Merge)

⛔ Merge 에서 Conflict 발생 시, 대부분 패닉에 빠지는데

Merge 롤백 : git merge --abort

(1) Fast-Forward Merge

  • Fast-Foward --ff (DEFAULT) : 로컬에 어떠한 작업도 없을때 원격 FETCH_HEAD 을 로컬 HEAD 로
    • 명령어 : git fetch + git merge origin/main 혹은 git merge **--ff** origin/main (충돌)
    • 설정 : git config pull.**ff** **only** (주의 : Fast-Forward 가 아니면 Pull 자체가 안되게 강제)
      • 동일 명령어 = git pull **--ff-only** origin main (Fast-Forward 가 불가능할 경우 거절됨)

실습 : Fast-Foward 실습

  • 같은 커밋을 기준으로, 원격 main 브랜치에서도 추가 커밋 + 로컬 main 브랜치에서도 추가 커밋
    • 아래와 같이 로컬 main 브랜치를 → 원격 main 브랜치로 Push 한다면 머지 충돌 발생

      > git push
      To github.com: 2SEONGA/git-asac-7th.git
      ! [rejected]
      main -> main (non-fast-forward)
      error: failed to push some refs to 'github.com: 2SEONGA/git-asac-7th.git'
      hint: Updates were rejected because the tip of your current branch is behind
      hint: its remote counterpart. Integrate the remote changes (e.g. 
      hint: 'git pull ...') before pushing again.
      hint: See the 'Note about fast-forwards' in 'git push --help' for details.
  • 이후 git pull **--ff-only** origin main 수행
    • = git fetch + git merge origin/main 혹은 git merge **--ff** origin/main
> git pull --ff-only origin main
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
renote: Total 3 (delta 1), reused O (delta O), packreused O (from 0)
Unpacking objects: 100% (3/3), 915 bytes | 228.00 KiB/s, done.
From github.com:aaronryu/hello-project
* branch               main          -> FETCH_HEAD
  eb7939a..905faa6     main          -> origin/main
hint: Diverging branches can't be fast-forwarded, you     need to either : 
hint: 
hint: git merge - -no-ff
hint: 
hint: or :
hint:
hint: git rebase 
hint:
hint: Disable this message with "git config advice.diverging false"
fital : Not possible to fast-forwarf, aborting.
  • 정상적으로 Fast-Forward 수행 시 아래와 같이 원격 FETCH_HEAD 를 로컬 HEAD 에 가져옴
    • 원격에서는 수정되었는데, 로컬에서는 어떠한 수정도 없는 경우 부드럽게 머지 완료
> git merge origin/main
Updating 713704a..a646151
Fast-forward
README.md | 3 +++
1 file changed, 3 insertions(+)
  • --ff-only 옵션의 경우엔 : Fast-Forward 조건에 맞지 않으면 → 동작되지 않지만
> git pull --ff-only ****origin main
From github.com:2SEONGA/git-asac-7th.git
* branch               main          -> FETCH_HEAD
hint: Diverging branches can't be fast-forwarded, you need to either: 
hint: 
hint:   git merge - -no-ff
hint: 
hint: or:
hint: 
hint:   git rebase
hint:
hint: Disable this message with "git config advice.diverging false"
fatal: Not possible to fast-forward, aborting.
  • --ff 옵션의 경우엔 : Fast-Forward 조건에 맞지 않으면 → --no-ff 로 동작
> git pull --ff-only ****origin main
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

(2) 3-Way Merge

  • 3-Way Merge --no-ff : 원격 FETCH_HEAD 와 HEAD 를 합친 새 Merge Commit 생성
    • 명령어 : git fetch + git merge **--no-ff** origin/main
    • 설정 : git config pull.**rebase** **false** (DEFAULT)
      • 동일 명령어 = git pull origin main

실습 : 3-Way Merge 실습

  • 같은 커밋을 기준으로, 원격 main 브랜치에서도 추가 커밋 + 로컬 main 브랜치에서도 추가 커밋
    • 아래와 같이 로컬 main 브랜치를 → 원격 main 브랜치로 Push 한다면 머지 충돌 발생
> git push
To github.com: 2SEONGA/git-asac-7th.git
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com: 2SEONGA/git-asac-7th.git' 
hint: Updates were rejected because the tip of your current branch is behind 
hint: its remote counterpart. Integrate the remote changes (e.g. 
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
  • 이후 git pull origin main 수행 = 바로 3-Way Merge 시도
    • = git fetch + git merge **--no-ff** origin/main
> git pull origin main
From github.com: 2SEONGA/git-asac-7th.git
* branch               main          -> FETCH_HEAD
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
> git status
On branch main
Your branch and 'origin/main' have diverged, 
and have 1 and 3 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)
      
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)
      
Unmerged paths:
  (use "git add < file>..." to mark resolution)
        both modified:    README. md
            
no changes added to commit (use "git add" and/or "git commit -a")

(3) Rebase and Merge

  • Rebase : 원격 FETCH_HEAD 를 기준으로 다시 Commit 들을 적재
    • 명령어 : git fetch + git **rebase** origin/main
    • 설정 : git config pull.**rebase** **true**
      • 동일 명령어 = git pull **--rebase** origin main (Rebase 가 불가능할 경우 거절됨)
> git pull --rebase origin main
From github.com: 2SEONGA/git-asac-7th.git
* branch               main          -> FETCH_HEAD
Auto-merging README.md
error: could not apply 05cb233... Local 7
hint: Resolve all conflicts manually, mark them as resolved with 
hint: "git add/rm <conflicted_files>" , then run "git rebase --continue"
hint: You can instead skip this commit: run "git rebase --skip". 
hint: To abort and get back to the state before "git rebase", run "git rebase - abort"
Could not apply 05cb233... Local 7           

수업 실습 : Rebase 실습

  • 같은 커밋을 기준으로, 원격 main 브랜치에서도 추가 커밋 + 로컬 main 브랜치에서도 추가 커밋
    • 아래와 같이 로컬 main 브랜치를 → 원격 main 브랜치로 Push 한다면 머지 충돌 발생
> git push
To github.com: 2SEONGA/git-asac-7th.git
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com: 2SEONGA/git-asac-7th.git' 
hint: Updates were rejected because the tip of your current branch is behind 
hint: its remote counterpart. Integrate the remote changes (e.g. 
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
  • 이후 git pull **--rebase** origin main 수행 = 바로 Rebase 시도
    • = git fetch + git **rebase** origin/main
> git pull -- rebase origin main
From github.com: 2SEONGA/git-asac-7th.git
* branch               main          -> FETCH_HEAD
Auto-merging README. md
CONFLICT (content): Merge conflict in README. md 
error: could not apply 05cb233... Local 7
hint: Resolve all conflicts manually, mark them as resolved with 
hint: "git add/rm ‹conflicted files>" , then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip" 
hint: To abort and get back to the state before "git rebase", run "git rebase
abort".
Could not apply 05cb233.. Local 7

(4) Squash and Merge

  • Squash Merge : 원격 커밋들을 모두 합쳐 새로운 Commit 생성 (상대의 커밋을 모두 파괴한다…)
    • 명령어 : git fetch + git merge **--squash** origin/main
    • 상대 커밋을 모두 파괴하기때문에 원격 브랜치에 대해서는 사용할 수 없고, 로컬 브랜치 머지에만 가능

어떤 Merge 방식을 사용하는것이 좋은가?

  • 가장 일반적으로는 3-Way Merge (Non Fast-Forward) 을 많이 사용하고, 주니어 개발자에게도 편하다.
    • 하지만 단점으로 앞서 언급했듯 부산물인 머지 커밋이 생긴다는 것
  • 머지 커밋이라는 부산물을 싫어하는 시니어 개발자들은 Rebase 를 통해 깔끔하게 커밋을 관리
  • Squash Merge 는 개인적으로 사용해본적도 없고, 굳이 그거 쓰느니 Rebase 잘 활용하면 같은 결과 가능

로컬 Git 내 영역에 대한 이해를 기반으로 Git 명령어 학습/복습

Git 내 영역별 의미 및 용례

    1. Remote
    1. Local
      1. Working Directory 작업공간 (1) : 로컬 작업공간에서 작업할 브랜치 선택 → git checkout
      • Tracked : Git 이 추적하는 파일
          1. Staging Area, Index (2) : Commit 되기위해 대기중인 파일 (대기조) → git commit
          • Git Commit 명령어 시 여기에 있는 파일들이 Commit 된다 (스냅샷 찍힘)
          • Cache = Index = Stage 혹은 Cached = Indexed = Staged
        • Unstaged (3) : Git 이 추적하는 파일 중 수정된 파일들의 집합
          • 이 중에서 Commit 하고자하는 것들을 Staging Area (2) 로 올린다
      • Untracked (4) : Git 이 추적하지 않는 파일


가장 기본적인 Git 작업 명령어 : 브랜치 관리 + 파일 관리

(0) Git 명령어 커스터마이즈

  • 필요 설정만 들어가있는 설정 파일 : .gitconfig
    • excludesfile = /Users/**aaron**/.gitignore_global 내부에는 DS_Store 방지 | 명칭 바꿀것

      ```
      .DS_Store
      ._.DS_Store
      **/.DS_Store
      **/._.DS_Store
      ```
      # ------- Default User Set --------
      [user]
      	name = Aaron Ryu
      	email = fbcndah@gmail.com
      	initials = aaron
      [http]
      	sslVerify = true
      [core]
      	autocrlf = input
      	whitespace= fix,-indent-with-non-tab,trailing-space,cr-at-eol
      	fscache = true
      	editor = vi
      	excludesfile = /Users/aaron/.gitignore_global
      # ------- Merge Tool --------
      [merge]
      	tool = vscode
      [mergetool "vscode"]
      	trustExitCode = false
      [diff]
          tool = vscode
      [difftool "vscode"]
          cmd = code --wait --diff $LOCAL $REMOTE
      # ------- Alias --------
      [alias]
      	co = checkout
      	ci = commit
      	st = status
      	br = branch
      	; ? = diff
      	l = !git graph | less -FXRS
      	r = !git graph -20 | less -FXRS
      	h = !git graph -1 | less -FXRS
      	graph = log --graph --date-order -C -M --pretty=format:\"%C(blue)%h%C(reset) (%ar) [%an] %C(yellow)%d%Creset %s\" --all --date=short
      	ls = log --graph -C -M --pretty=format:"%C(yellow)%h%Cgreen%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --all --date=short
      	ll = log --pretty=format:"%C(yellow)%h%Cgreen%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --all --numstat
      	del = "!f() { git branch -d \"$1\" && git push origin --delete $1; }; f"
  • (위 파일에서) 각 옵션 별 설명이 상세하게 추가되어있는 설정 파일 : .gitconfig
    # ------- Default User Set --------
    [user]
    	name = Aaron Ryu
    	email = fbcndah@gmail.com
    	initials = aaron
    #    name = Kim Doyeon
    #    email = doyeon311@gmail.com
    #    initials = kimdoyeonn
    [http]
    	sslVerify = true
    [core]
    	autocrlf = input
    	; 	if true, git considers that 'CR' is added, 
    	; 	when 'CRLF' in working directory --> 'LF' in Repo
    	; safecrlf = true
    	; 	when [autocrlf = false] git reject commit
    	; 	- Commit: CRLF -> LF
    	; 	- Checkout: LF -> CRLF
    	; 	Consume there is only 'LF' in repository
    	whitespace= fix,-indent-with-non-tab,trailing-space,cr-at-eol
    	; preloadindex = true [default]
    	fscache = true
    	editor = vi
    	excludesfile = /Users/aaron/.gitignore_global
    # ------- Colors --------
    [color]
    	; ui = true [default]
    	; interactive = true [default]
    [color "diff"]
    	; meta = blue white bold
    # ------- Merge Tool --------
    [merge]
    	tool = vscode
    [mergetool "vscode"]
    	trustExitCode = false
    [diff]
        tool = vscode
    [difftool "vscode"]
        cmd = code --wait --diff $LOCAL $REMOTE
    # ------- Alias --------
    [alias]
    	co = checkout
    	ci = commit
    	st = status
    	br = branch
    	; ? = diff
    	l = !git graph | less -FXRS
    	r = !git graph -20 | less -FXRS
    	h = !git graph -1 | less -FXRS
    	graph = log --graph --date-order -C -M --pretty=format:\"%C(blue)%h%C(reset) (%ar) [%an] %C(yellow)%d%Creset %s\" --all --date=short
    	ls = log --graph -C -M --pretty=format:"%C(yellow)%h%Cgreen%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --all --date=short
    	ll = log --pretty=format:"%C(yellow)%h%Cgreen%d\\ %Creset%s%Cblue\\ [%cn]" --decorate --all --numstat
    	; standup = log --since yesterday --oneline --author <who@you-want-to-know.com>
    	del = "!f() { git branch -d \"$1\" && git push origin --delete $1; }; f"
    # ------- Additional --------
    [heroku]
    	account = work
    	; Cloud PaaS like AWS, give us 1 Free host
    [gitopen "gitlab"]
    	domain = http://my.own.page.net/project/
    	; git clone and add to ./zshrc
    	; git clone https://github.com/paulirish/git-open.git $ZSH_CUSTOM/plugins/git-open
  • git config --list 명령어를 통해 우리가 설정한 내용들이 잘 적용되어있는지 일괄 확인이 가능

(1) 브랜치 관리

  • git init : .git 생성 및 시작
fatal: not a git repository (or any of the parent directories): .git
  • git fetch -p : 원격 브랜치를 로컬로 가져오기 (원격-로컬 브랜치 동기화)
  • git pull origin main : 원격 main 브랜치의 최신 커밋들을 로컬로 가져오기 (Pull + Merge)
  • git branch -a : 원격 브랜치와 로컬 브랜치 리스트 조회
  • `git checkout [기존 브랜치명]` : 로컬 브랜치 이동
  • `git checkout -b [새 브랜치명]` : 로컬 브랜치 생성 (현재에 위치하고 있는 브랜치와 커밋을 기준으로)

실습 : 로컬 프로젝트에서 Git 활성화 후 새 원격 저장소에 Push → 새로운 브랜치 만들기

  • git clone : 원격 저장소의 코드 = 브랜치들을 로컬로 가져오기
  • git remote -v : 원격 저장소 조회
  • git fetch : 원격 저장소에서 생성한 브랜치 조회
  • git branch -a : 생성된 브랜치 포함 모든 브랜치 조회 (원격 브랜치에 대한 Reference 확인)
  • git fetch -p : 원격 브랜치에서 삭제한 브랜치 조회
  • git branch -a : 삭제된 브랜치 포함 모든 브랜치 조회 (원격 브랜치에 대한 Reference 확인)
  • git checkout -b [새 브랜치명] : 새 로컬 브랜치 생성
  • git branch -a : 생성된 브랜치 포함 모든 브랜치 조회
  • git checkout : 로컬 브랜치 이동 - 다시 main 브랜치로 돌아가기 >

(2) 파일 관리

  • git add [파일명] : Git 커밋에 수정 사항으로 등록하기 위한 예비 절차 = Staged : Commit 대기 (예비)
    • StagedUnstaged : Git 이 추적중인 파일의 변경사항
    • StagedUntracked : Git 이 모르는(추적하지 않아 몰랐던) 파일의 추가
  • git add -p : 파일 단위의 추가가 아닌 텍스트 단위의 추가 (아래에서 더 상세하게 사용법을 배울 것)
  • git add . : 변경된 파일 모두 추가 (생각보다 이것만 사용)

실습 : 새 커밋 만드는 방법 = 새로운 파일 “추가하기” + 기존 파일 “수정하기”

  • touch README.md : 새로운 파일 “추가하기”
  • git status + git add : Untracked = Git 이 추적하지 않아서 처음보는 파일
  • echo 'main' >> README.md : 기존 파일 “수정하기”
  • git status + git add : Unstaged → Staging = Git 이 추적하고있어 알고있는 파일
  • touch hello-world.sh + echo "echo 'Hello, World!'" > hello-world.sh
    • ./hello-world.sh : 실행이 되지 않는다 = Permission Denied 실행 권한이 없는것
    • chmod 700 ./hello-world.sh : 실행 권한을 부여해주자
    • 이 파일은 Git 내 포함시키고싶지 않다 = 하지만 계속 Untracked 로 보이는것이 걸리적거림
  • git status : hello-world.sh 파일이 Untracked 로 보임 = 걸리적거림
  • touch .gitignore + echo '**/hello-world.sh' >> .gitignore
    • * 두 개와 한 개의 차이?
      • ** : 0개 이상의 디렉토리 및 모든 하위 디렉토리
        • : 단일 디렉토리 또는 파일 이름에서 0개 이상의 문자와 일치
  • git status : hello-world.sh 파일이 보이지않음 + .gitignore 가 Untracked 로 보임
  • git add + git commit : .gitignore 파일을 Git 에 추가

  • 추가로 git add -p 수행하려면 중간중간 작업물이 들어가야해서 시간이 걸림

(3) 파일 수정 내용 임시 저장소 : Stash

작업중에 잠깐 지금의 작업을 어딘가에 임시 저장하였다가, 나중에 다시 활용하고 싶을 때

  • 예시 : A 브랜치에서 작업하던 작업물 잠깐 저장 후, B 브랜치로 이동하여 커밋 조작 후 다시 이어 작업
  • git stash : 현재 git add 를 통해 Commit 대기중인 모든 수정사항들 임시 저장소에 저장
  • git stash pop : 임시 저장소에 저장되어있는것중 가장 최신의 하나를 꺼냄 (Stack 자료구조)
  • git stash list : Stack 자료구조에 쌓여있는 임시 저장본 리스트 조회

실습 : 앞 실습서 main 브랜치로 돌아왔을텐데 작업하다 Stash 로 feature/1 브랜치로 이동

  • 변경사항이 별로 없을땐 단순 git checkout 만으로도 바로 이동가능하나 보편적 케이스는 아님
  • 파일을 수정했을때 Unstagedgit stash : 스태싱 O = 변경사항이기 때문
  • 파일을 추가했을때 Untrackedgit stash : 스태싱 X = 변경사항이 아니기 때문

(4) 커밋 생성 및 수정

  • git commit : 에디터를 통해 메세지 입력하여 커밋 (일반적으로는 vi 사용, 커스텀 시 code 사용 가능)
  • git commit -m "메세지" : 입력한 메세지를 통해 (귀찮게 에디터 켜지 않고) 바로 커밋
  • git commit -am "메세지" : git commit -a (Untracked 제외) + git commit -m "메세지"
  • git commit --amend : 앞선 커밋 수정 (커밋 메세지 수정 or 커밋 내용 추가)
  • git push : 현재 브랜치에 커밋된 내용 모두 원격에 업로드(푸시)
  • git push -u origin main : 로컬 브랜치에 연결된 원격 브랜치가 없을때 최초 연결
    • = git push --set-upstream-to origin main

실습 : 앞 실습서 main 브랜치로 돌아왔을텐데 작업하다 Stash 로 feature/1 브랜치로 이동

  • 파일을 수정했을때 Unstagedgit commit -am 시 적용 : 커밋되는것을 확인 가능
  • 파일을 추가했을때 Untrackedgit commit -am 시 미적용 : 커밋되지 않는것을 확인 가능

실습 : 직전 단일 커밋을 수정해야할때 git commit **--amend**
1. 커밋 메시지 수정 케이스
2. 커밋에 넣어야하는데 놓친 수정 내용을 기존 커밋에 합치는 케이스

(5) 커밋 삭제 (롤백)

  • git reset HEAD~1 : 현재 커밋 Mixed 롤백 = 커밋했던 수정본 모두 다시 커밋되지 않은 상태로 롤백
  • git reset --hard HEAD~1 : 현재 커밋 Hard 롤백 = 커밋했던 수정본 (흔적도 없이) 싹다 없애고 롤백

  • 절대 커밋 지칭 Hash (SHA-1, 줄여쓰기) : 2cd27c2427c904c6ec4d30ce28a633e4d5113245f
  • 상대 커밋 지칭 Relative References (^ 과 ~) : HEAD~1 혹은 HEAD^1
    • HEAD 상대 표현법
      • ~ (Tilde) : N번째 조상 - 수직
      • ^ (Caret) : N번째 부모 - 수평
      • @ (At-Sign) : Reflog 내가 직접 커밋한 히스토리 기반의 롤백

실습 : 이전 다수 커밋을 롤백해야할때 = git **reset**

  • git reset **--hard** HEAD~1 : 아예 흔적까지 없애버리는것 = 싹 다 롤백
  • git reset **--mixed** HEAD~1 = git reset HEAD~1 (Default) : 작업물 그대로 남긴채 롤백
  • git reset **--soft** HEAD~1 : 작업물 그대로 남긴채 롤백 (Staging = 바로 커밋 가능)

(6) 커밋 조회

  • git log : 커밋 메세지 기반 조회
  • git show : 커밋 메세지 + 상세 수정내용 기반 조회

(7) 몇 가지 에러 혹은 경고 메세지

  • 윈도우/리눅스 OS 마다 사용되는 줄바꿈 문자열이 다르기 때문에 Git 에서 이에 대한 처리를 하겠다는 뜻
    • 윈도우 : git config --global core.**autocrlf** **true**

    • 리눅스 : git config --global core.**autocrlf** **input**

      warning: in the working copy of 'detail.html', LF will be replaced by CRLF the next time Git touches it.
      warning: in the working copy of 'simple.html', LF will be replaced by CRLF the next time Git touches it.

시나리오로 정확히 이해하는 Git 명령어 : Remove, Add, Restore

0. Git 3개 영역 이해하기

  • Working Directory 작업공간 : 로컬 작업공간에서 작업할 브랜치 선택 → git checkout
    • Working Directory 작업공간 내 Staging Area / Unstaged / Untracked
  • 첫번째 초록색 블럭 = Staging Area : Commit 되기위해 대기중인 (1) 추가 + (2) 수정 + (3) 삭제된 파일
    • (1) new file: + (2) modified: + (3) deleted:
    • git commit 명령어 시 여기에 있는 파일들이 Commit 된다 (스냅샷 찍힘)

  • 두번째 빨간색 블럭 = Unstaged : Git 이 추적하는 파일 중 (2) 수정 + (3) 삭제된 파일들의 집합
    • (2) modified: + (3) deleted:
    • 이 중에서 Commit 하고자하는 것들을 Staging Area 로 올린다 → git add

  • 세번째 빨간색 블럭 = Untracked : 아직 Git 이 추적하지 않는 파일 (새로 (1) 추가된 파일)
    • (1) new file:
    • 이 중에서 Commit 하고자하는 것들을 Staging Area 로 올린다 → git add
      • Commit 하고싶지 않은 임시 혹은 중요 파일들은 ⇒ .gitignore 에 추가

1. 파일 수정 시나리오

  • Tracked : Git 이 추적하는 파일 중 수정 시나리오

  • 모든 파일 변경 내용은 Unstaged 상태로. Commit 할것들만 선택적으로 Staging Area 로 전달
    • git **add** <file> : 변경된 파일 하나만 지목하여 Staging Area 로 전달
    • git **add** . : 모든 파일 변경 사항들을 한번에 Staging Area 로 전달
    • git **add** -p : 모든 파일 변경 사항들을 작은 단위로 세부적으로 확인하며 Staging Area 로 전달
      • 작은 단위를 Hunk 라고 부르며 몇가지 옵션을 통해 손쉽게 활용 가능
        • `y = yes` : 현재 보고있는 hunk 를 add(stage)
        • `n = no` : 현재 보고있는 hunk 를 add(stage) 하지 않음
        • `q = quit` : 현재 add(stage) 과정 종료 = add 할 게 끝나서 더 이상 볼 필요 없을 때
        • `s = small` : 현재 보고있는 hunk 를 더 작은 단위의 hunk 로 나눔
        • `e = edit` : 현재 보고있는 hunk 내용을 직접 편집
  • Staging Area 로 전달한것을 다시 복구시키고싶다면
    • git **restore --staged** <file> : Staging Area → Unstaged (git add 이전)
    • git **restore** <file> : Unstaged → (Unmodifed) (수정 이전)

실습 : 파일 수정 시나리오 = Unstaged ↔ Staging

  • echo 'example' >> README.md : 기존 Git 이 추적하고있는 파일의 수정 = Unstaged
  • git add README.md : Commit 을 위한 대기조 = Staging ← Unstaged
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Unstaged
  • git add README.md : Commit 을 위한 대기조 = Staging ← Unstaged (이해를 위한 반복)
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Unstaged
  • git restore README.md : 해당 파일의 수정된 내용을 Git 이 추적하는 이전 스냅샷으로 롤백
    • Unstaged 의미는 Git 이 추적하는 파일의 이전 스냅샷에서 바뀌었다는것이므로
    • restore 명령어를 통해 이전 스냅샷으로 돌아간다는 것 = 현재 수정 내용을 롤백한다는 것

2. 파일 추가 시나리오

  • Untracked : Git 이 추적하고있지 않은 파일 추가 시나리오

  • 파일을 추가 시 Git 이 전혀 알지못하는 새로운 파일이라 Untracked 에 추가된다
    • 추가한 파일을 내 로컬에서만(나만) 사용할것인지? Git 에 등록할것인지? 검토 필요
      1. 내 로컬에서만(나만) 쓸거라면 Git 에 추적되지 않도록 .gitignore 내 등록 : 민감 정보 등
      2. Git 에 등록할것이라면 git **add** 를 통해 Staging Area 로 전달
  • Staging Area 로 전달한것을 다시 복구시키고싶다면
    • git **restore --staged** <file> : Staging Area → Untracked (git add 이전)

실습 : 파일 추가 시나리오 = Untracked ↔ Staging

  • touch example : 기존 Git 이 추적하고있지 않은 파일의 추가 = Untracked
  • git add example : Commit 을 위한 대기조 = Staging ← Untracked
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Untracked
  • git add example : Commit 을 위한 대기조 = Staging ← Untracked (이해를 위한 반복)
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Untracked
  • git add example + git commit : 바로 아래에서 파일 삭제 시나리오 실습을 위해 Commit

3. 파일 삭제 시나리오 : Remove(rm)

  • Tracked : Git 이 추적하는 파일 중 삭제 시나리오

  1. 명령어로 삭제하기 git **rm** <file>
    • Staging Area 로 바로 올려 Commit 시 바로 삭제되도록
      • Git Repository 에서 삭제 예정(Staging) + Local 에서 삭제
  2. 파일을 그냥 삭제하기 (마우스 우클릭이든, 리눅스 명령어든)
    • Unstaged 에 올려 개발자가 한번 검토한 후 Staging Area 에 올리도록
      • Git Repository 에서 삭제 검토(Unstaged) + Local 에서 삭제
  3. 명령어로 삭제하기 git **rm --cached** <file>
    • Staging Area 로 바로 올려 Commit 시 바로 삭제되도록함과 동시에 Untracked 에서 나만 보기
      • Git Repository 에서 삭제 예정(Staging) + Local 에서 사용
        • Git 에서만 확실하게 없애고, 내 로컬에서 나만 사용할 파일에 사용
          • 민감 정보가 포함되어있는 Credential 이나 .env 과 같은 개인 설정 파일들

실습 : 파일 삭제 시나리오 = Unstaged ↔ Staging ↔ Untracked

  • rm example : 기존 Git 이 추적하고있는 파일의 삭제 = Unstaged
  • git add example : Commit 을 위한 대기조 = Staging ← Unstaged
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Unstaged
  • git add example : Commit 을 위한 대기조 = Staging ← Untracked (이해를 위한 반복)
  • git restore --staged README.md : Commit 대기조에서 제외 = Staging → Untracked
  • git restore example : 해당 파일이 삭제된 것을 Git 이 추적하는 이전 스냅샷으로 롤백
    • Unstaged 의미는 Git 이 추적하는 파일의 이전 스냅샷에서 바뀌었다는것이므로
    • restore 명령어를 통해 이전 스냅샷으로 돌아간다는것 = 현재 삭제된걸 롤백한다는것

  • git rm example : 기존 Git 이 추적하고있는 파일의 삭제 + Commit 대기조 = Staging

  • git rm --cached example : 기존 Git 이 추적하고있는 파일의 삭제 + 개인적으로 파일 사용
    • = Staging + Untracked

4. Git Add 와 Restore : 파일 추가, 삭제, 수정 시나리오 별

앞서 배우고 실습했던 내용들을 합치면 검은색 화살표 ↔ 주황색 화살표 이 2개가 서로 반대라는것만 알면됨

  • Git ADD : 어떤곳에서든 Staging Area 로 보낼때 사용
  • Git RESTORE : Staging Area 혹은 Unstaged 에서 이전 상태로 롤백 시 사용
    1. --staged 옵션에 따라 Staging Area 에서 롤백할지 Unstaged 에서 롤백할지 결정
    2. 추가, 삭제, 수정 시나리오에 따라 이전 상태가 달리 정의
      • --staged 옵션이 있으면 : Staging Area 에서 롤백
        • 삭제할 파일의 이전 상태 = Unstaged 에서 대기 : Staging Area 에서 → Unstaged 로 이동
        • 수정된 파일의 이전 상태 = Unstaged 에서 대기 : Staging Area 에서 → Unstaged 로 이동
        • 추가된 파일의 이전 상태 = Untracked : Staging Area 에서 → Untracked 로 이동
      • --staged 옵션이 없으면 : Unstaged 에서 롤백 (아무일도 없던것처럼 깨끗히 롤백)
        • 삭제할 파일의 이전 상태 = 미삭제(존재) : Unstaged 에서 → 기존 Git 이 추적하던 파일 존재
        • 수정된 파일의 이전 상태 = 미수정 : Unstaged 에서 → 기존 Git 이 추적하던 파일의 이전 상태

5. Git Commit 메세지도 아무 생각없이 적어 커밋하지 말 것

  • 할당된 티켓이 명확히 있다면 그 티켓명을 앞에 적어주고, 어떤 작업인지에 대한 요약 설명을 제목으로
    • 예) [ASAC-001] 테스트 코드 작성을 위한 준비

(선택) 그 아래엔 어떤 세부 작업이 있는지 나열하는 것 추천 → 구체적일 필요는 없으나 언제봐도 알 수 있게

[HELLO-001] 테스트 코드 작성을 위한 준비

- 어떤어떤 작업을 수행
- 어떤어떤 값들을 추가하여 어떤어떤 작업을 도움
- 어떤어떤 값을 변경
  • 할당된 티켓을 명시하지 않고, 어떤 작업인지에 대한 타입을 앞에 명시하고, 상세 내용 작성
    • 예) Feat: 테스트 코드 작성을 위한 준비


Git 트러블슈팅 : 이전 Commit 수정

1. Amend / Reset - 단일 문제 발생 : 바로 직전 커밋을 수정

  1. Amend : 직전 커밋에 대한 간단 수정 - git commit --amend
  2. Reset : 직전 커밋 롤백 및 다시 커밋 생성 - git reset HEAD~1
    • Cherry-Picking 불가능 : 예) git reset **HEAD~3** : 3번째 전으로 3개의 커밋 모두 롤백
    • Reset 세부옵션
      • --soft : Staging Area
      • --mixed (Default) : Unstaged/Untracked
      • --hard : 완전히 없애는것

2. Rebase - 다수 문제 발생 : 이전 커밋들을 다시 재정의할때

  • Rebase : 다수의 이전 커밋들을 다시 재정리 - git rebase -i HEAD~3
    • Cherry-Picking 가능 : 예) 이전 5개의 커밋 중 2개는 지우고, 2개는 합치고, 1개는 메세지만 수정하고
    • Rebase 세부옵션
      • Reword : 커밋 메세지 변경
      • Edit : 커밋 내용 변경 및 충돌 발생 시 추가 작업을 위한 Rebase 작업
      • Drop : 커밋 빼기
      • Squash : 불필요하게 많아진 커밋의 수를 줄이기 위해 몇 커밋 합치기

3. Reflog - 날려먹었을때 : 이전 작업 원상복구

  • Reflog : 이전에 내가 작업했던 커밋이나 브랜치로 이동하기 (백업) - git reflog
    • 수행하는 모든 커밋들은 저장되어 어떤 치명적인 문제가 발생해도 대응할 수 있다.
    • HEAD 상대 표현법 중 Reflog 에서 표기되는 @ (At-Sign)
      • @ (At-Sign) : Git 명령어를 수행하며 HEAD 가 변경되기의 이전 HEAD (ORIG_HEAD 로 불림)
    • git reflog 통해 이전에 수행했던 히스토리들을 쭉 살펴보고 원하는 커밋의 SHA 해시값을 봤으면
    • 해당 해시값으로 현재 브랜치를 이동시킬 수 있고 : git reset --hard 349b40d
    • 해당 해시값으로 새 브랜치 생성도 가능 : git checkout -b backup 349b40d

테스트 및 배포를 위한 Zone 구성 및 Git 브랜치 전략

개발 결과물 테스트를 위한 Develop Zone실제 유저들이 사용하기 위해 배포하는 Production Zone

그리고 이러한 Zone 과 연동하여 관리하는 Git 브랜치 전략(git-flow)

1. 현업에서의 테스트 및 배포를 위한 Zone 구성

부트캠프 혹은 학생들의 개인, 팀 프로젝트들은 Zone 구별없이, 로컬에서 테스트하거나 최종 배포하여 테스트

현업에서는 실제 유저 사용에 있어 문제가 발생하지 않도록 테스트를 꼼꼼히 진행하며, 문제 발생 시 롤백 전략

  • Local : 실제 개발을 진행하며, 디버깅 및 로컬 테스트를 수행하는 로컬 환경
    • Local 데이터베이스 : 가벼운 테스트를 위해 개발자가 직접 적재 혹은 로컬 실행을 통해 적재되는 정보
      • 가끔은 개발자간 데이터베이스 내 데이터 일관성을 위해 Migration SQL 구문으로 주고 받음
      • 원시적인 방법으로 CSV(Comma Seperated Value) 파일을 주고받을 순 있으나 파일 크키가 큼
    • Localhost 웹 브라우저 를 통한 Frontend 테스트 (예, React 의 Hot Loader 활용한 실시간 디버깅)
    • Localhost Postman 를 통한 Backend 테스트 (예, Spring 으로 개발한 API)
  • Develop Zone : 마무리된 개발에서 누락된 테스트 케이스는 없을지, 내부 사용자(개발자)에게 노출(배포)
    • Develop Zone 데이터베이스 : 테스트를 위해 개발자들이 직접 적재 혹은 테스트를 통해 적재되는 정보
      • 로컬에서보다 적재된 데이터량이 많고, 개발자들이 테스트를 진행하여 더 다양한 유즈케이스 커버
      • 개발자간 데이터를 주고받을 필요없이 마음껏 단일 Develop DB 에 적재 가능
    • 로컬에서 개발을 완료했다는것 : 개발자 개인이 수행해볼 수 있는 최대한의 유즈케이스를 커버했다는 뜻
      • 주의 : Develop Zone 에서 버그가 발생하고, 그곳에서 버그를 찾는걸 너무 당연하게 생각하지 말자
      • 누군가가 먹을 수 있는 수준의 음식을 만들고 시음을 하지, 시음으로 맛을 찾아가지 않음
        • 어쨌거나 로컬만이 세부 디버깅 및 로그 확인이 가능한 가장 투명한 디버깅 공간
      • Frontend 입장 : 내가 만든 프론트엔드가 다양한 (개발자) 유저에 잘 표기될 지
      • Backend 입장 : 내가 만든 API 가 백엔드, 프론트엔드가 실제 호출했을때 문제 없을 것인지
  • Production Zone : 실제 유저가 사용하는 환경으로 개발 및 수정 등에 신중해야하며, 제약이 심하다.
    • Production Zone 데이터베이스 : 실제 유저가 사용하는 운영 DB
      • 데이터베이스에 대한 ALTER 명령어는 매우 신중해야한다. 리인덱싱 이슈 등
      • 이상적으로는 백업 전략을 통해 치명적인 문제가 발생할 경우 돌아갈 DB 스냅샷이 필요
        • 언제나 그렇듯 대체의 환경과 기업에서 이상적인 상황을 적용하지는 않는다. 현실과의 타협
    • 개발과 테스트를 완료한 경우, 실제 유저들이 해당 기능을 사용할 수 있게 최종 배포를 해야함
  • Staging Zone : 운영존에 가장 가까운 환경, 최종 배포전 네트워크 및 인프라 환경에 대한 마지막 테스트
    • Staging Zone 데이터베이스 : 운영존과 가장 가까운 수준의 데이터량을 적재
      • 일반적으로 Develop Zone 과 Production Zone 의 DB 에 적재된 데이터량의 편차가 심함
      • 비유를 하자면 Develop Zone 데이터량이 100 이면, Production Zone 데이터량은 10000 이상
      • 수많은 데이터를 직접 적재하기 힘들기 떄문에, 아래서 배울 운영존 DB 에서 동기화해옴
        • 단, Sanitizing 이란것을 통해 개인정보들에 대해 난수화 - 개인정보보호 법적 보호
    • 하나의 버그가 치명적인 상황(경제적 이슈)을 불러일으키는 거대 유저를 보유한 기업의 경우에 많이 구축
    • 스타트업 혹은 중견기업에선 Staging Zone 이 없다. 혹은 있다하더라도 개인정보 보호가 미약할 것
      • 비용의 문제 : 운영존과 비슷한 환경을 구성하기 위해 사용해아하는 AWS 비용은 운영만큼 듦
    • Staging Zone 데이터베이스 : 운영존과 가장 가까운 수준의 데이터량을 적재
      • 일반적으로 Develop Zone 과 Production Zone 의 DB 에 적재된 데이터량의 편차가 심함
      • 비유를 하자면 Develop Zone 데이터량이 100 이면, Production Zone 데이터량은 10000 이상
      • 수많은 데이터를 직접 적재하기 힘들기 떄문에, 아래서 배울 운영존 DB 에서 동기화해옴
        • 단, Sanitizing 이란것을 통해 개인정보들에 대해 난수화 - 개인정보보호 법적 보호
    • 하나의 버그가 치명적인 상황(경제적 이슈)을 불러일으키는 거대 유저를 보유한 기업의 경우에 많이 구축
    • 스타트업 혹은 중견기업에선 Staging Zone 이 없다. 혹은 있다하더라도 개인정보 보호가 미약할 것
      • 비용의 문제 : 운영존과 비슷한 환경을 구성하기 위해 사용해아하는 AWS 비용은 운영만큼
  • Production Zone : 실제 유저가 사용하는 환경으로 개발 및 수정 등에 신중해야하며, 제약이 심함
  • Production Zone 데이터베이스 : 실제 유저가 사용하는 운영 DB
    • 데이터베이스에 대한 ALTER 명령어는 매우 신중해야한다. 리인덱싱 이슈 등
    • 이상적으로는 백업 전략을 통해 치명적인 문제가 발생할 경우 돌아갈 DB 스냅샷이 필요
    • 언제나 그렇듯 대체의 환경과 기업에서 이상적인 상황을 적용하지는 않고 현실과의 타협
    • 개발과 테스트를 완료한 경우, 실제 유저들이 해당 기능을 사용할 수 있게 최종 배포를 해야함

2. Git 브랜치 전략 (git-flow)

원칙은 모든 배포는 격리된 브랜치에서 작업을 완료한 뒤 PR 를 통해 배포를 위한 브랜치에 머지해야한다.

중요도에 따라 3가지 타입의 브랜치로 나누어진다.

  • Master / Main : 배포 브랜치
    • 버그 발생에 대한 불확실성이 낮고, 무결하여 운영존에 치명적 문제 발생 시 롤백할 수 있는 커밋 집합
  • Develop (Staging) : 테스트 브랜치
    • ‘테스트’ 의 의미는 다시 한번 강조하지만, 로컬에서 테스트 및 개발을 완료한 뒤 마지막 테스트
    • Staging Zone 을 위한 Staging 브랜치를 따로 또 둘 수는 있으나 조금은 과한 정책이라 할 수 있다
  • Feature : 개발 브랜치 - Develop 브랜치를 기점으로 새 브랜치를 만들어 개발
    1. feature/**HELLO-001** : 기획과 디자인에 따라 개발자에게 발행된 티켓으로 브랜치명
    2. feature/**login-with-oauth** : 어떤 작업인지 바로 알 수 있도록, 기능 내용에 대한 요약 브랜치명
      • aaron/**login-with-oauth** : 브랜치명에 개발자 명을 붙여넣기도함
    • 로컬에서 개발을 완료한 경우 Develop 브랜치에 PR 요청을 하여 마지막 테스트를 준비한다.
      • PR 요청을 통해 로컬에서 완료한 개발 코드를 개발자들에게 최초 노출하여 코드 리뷰를 받을 수 있고
      • PR 요청을 통해 Github Workflow 로 CI (테스팅) 파이프라인을 연결하였다면, 코드 유효성 테스트
profile
(와.. 정말 Chill하다..)

0개의 댓글