안녕하세요. 우아한형제들 앱서비스팀에서 iOS 앱을 만들고 있는 김민호입니다. 이번에 저희 팀에서 코딩 스타일을 맞추기 위해 Git Hook을 적용한 경험을 공유드리려고 합니다.

iOS 앱을 만들고 있는 많은 팀에서 코딩 스타일을 관리하기 위해 SwiftLint를 도입해서 사용하고 있는데요. 저희 팀 역시 SwiftLint를 이용하고 있지만 엄격하게 적용하고 있지는 않고 권장하는 정도로 사용하고 있었습니다.

그러다 점점 프로젝트 참여 인원이 많아지고, 코드 리뷰에 쓰는 시간도 같이 늘어나면서 일이 많을 땐 조금은 부담이 되기도 하는데요.

앞으로도 참여 인원이 계속 늘어날 걸 감안하면 리뷰하는 시간을 좀 더 효율적으로 만들 필요성을 느꼈고, 이를 위해 팀원들간에 익숙한 코딩 스타일을 통해 보다 기능적인 부분에 집중할 수 있도록 해보고자 Git Hook을 적용하기로 했습니다.

Git Hook?

먼저 Git Hook은 Git을 사용해서 코드를 commit, push, receive 하기 전/후에 이벤트를 수행하도록 하는 스크립트인데요.
built-in 기능이기 때문에 따로 다운 받을 필요 없이 git init을 통해 프로젝트 폴더 안에 자동으로 스크립트 파일이 만들어진다고
합니다.

참고로 Git 2.9 버전부터 hooks 폴더 이름과 위치가 조금 달라졌는데요.

git init을 하면 프로젝트/.git/hooks에 생성되고, 여기에서 스크립트 파일을 관리했지만 2.9 버전부터는 프로젝트/.githooks에 사용할 스크립트 파일을 넣으면 됩니다.

저희 팀은 .git 폴더가 gitignore 항목에 포함되어 있었기 때문에 번거로운 작업을 피하고자 Git 2.9버전 부터 지원하는 방법을 쓰기로 했습니다.

Hook 추가

저희 팀원은 모두 2.9 버전 이상을 사용하고 있기 때문에 Xcode - Build Phases - SwiftLint 스크립트가 동작할 때 hooks 폴더가 없을 경우 생성하도록 만들어보았습니다.

if which swiftlint >/dev/null; then
    if [ "$CONFIGURATION" ]; then
        PROJECT_PATH="${PWD%/*}"
        HOOKS_PATH=$(git config --get core.hooksPath)
        if [ "$PROJECT_PATH/.githooks" != "$HOOKS_PATH" ]; then
            git config core.hooksPath $PROJECT_PATH/.githooks
        fi
    fi
    swiftlint
else
    echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi  

$CONFIGURATION 의 경우는 개발/배포 같이 특정 환경에서만 SwiftLint를 실행하고 싶을 경우 $CONFIGURATION == Debug 같은 형태로 작성해주시면 됩니다.

그 다음 할 일은 어떤 훅을 사용할지 결정해야 하는데요.

저희는 기본으로 제공되는 훅 중에서 코드를 커밋할 때 컨벤션을 체크하기 위해 pre-commit 훅을 사용했습니다.

pre-commit 훅은 커밋할 때 가장 먼저 호출되는 훅으로 커밋 메시지를 작성하기 전에 호출된다 고 되어 있네요.
(다른 훅이 궁금하시다면 https://git-scm.com/book/ko/v2/Git맞춤-Git-Hooks 여기를 참고해주세요.)

pre-commit 훅을 사용하기 위해선 pre-commit.sample 파일의 .sample이라는 확장자를 제거하는 것 만으로 사용이 가능해집니다.

SwiftLint와 Git Hook을 이용한 테스트

이제 사용은 가능하지만 아직 아무런 기능이 없기 때문에 다음 할 일은 pre-commit 파일 안에 SwiftLint를 실행하는 코드를 작성했습니다.

LINT=$(which swiftlint)

if [[ -e "${LINT}" ]]; then
  echo "SwiftLint Start"
else
  echo "SwiftLint does not exist, download from https://github.com/realm/SwiftLint"
  exit 1
fi
count=0

targets=$(git diff --stat --cached --diff-filter=d --name-only $(git for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)) | grep -F ".swift")
addModifiedFile "${targets[0]}" $count
export -p | grep SCRIPT_INPUT_FILE
export SCRIPT_INPUT_FILE_COUNT=$count
RESULT=$($LINT lint --use-script-input-files --path "경로" --config .precommitlint.yml)
if [ "$RESULT" == '' ]; then
  printf "SwiftLint Finished.\n"
else
  echo ""
  printf "SwiftLint Failed. Please check below:\n"

위의 코드를 간단히 설명드리면 먼저 SwiftLint가 설치되어있는지 확인하고, 설치되어 있다면 SwiftLint를 통해서 수정된 모든 파일을 체크하게 됩니다.

그리고 코드를 커밋할 경우, 빌드할 때 사용하는 룰 보다 강화된 룰을 적용하기 위해서 별도의 yaml 파일을 사용하기 위한 옵션으로 lint --config .precommitlint.yml 처럼 별도의 파일을 지정해주었는데요.

너무 많은 룰을 적용하게 되면 빌드 타임에도 영향을 줄 수 있기 때문에 가급적이면 빌드할 때는 부담을 줄이려는 의도입니다.

그 외의 메시지를 꾸며준다던가 하는 부분들은 기호에 맞게 수정해서 사용할 수 있습니다.

이제 기대한 대로 돌아가는지 커밋을 해보았습니다.

SwiftLint Failed. Please check below:

  warning
    /Users/kimminho/baemin/HomeViewController.swift:385:56
    Opening Brace Spacing Violation - Opening braces should be preceded by a single space and on the same line as the declaration. (opening_brace)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:584:24
    Opening Brace Spacing Violation - Opening braces should be preceded by a single space and on the same line as the declaration. (opening_brace)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:31:17
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:32:17
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:192:73
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:239:57
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:291:35
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:385:55
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:565:20
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:566:13
    Colon Violation - Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. (colon)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:29:27
    Operator Usage Whitespace Violation - Operators should be surrounded by a single whitespace when they are being used. (operator_usage_whitespace)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:30:27
    Operator Usage Whitespace Violation - Operators should be surrounded by a single whitespace when they are being used. (operator_usage_whitespace)

  warning
    /Users/kimminho/baemin/HomeViewController.swift:239:108
    Comma Spacing Violation - There should be no space before and one after any comma. (comma)

Your commit is rejected. You have to fix it before committing again.

코딩 스타일을 맞추지 않은 코드가 있기 때문에 커밋에 실패했고, 룰을 위반한 swift 파일 이름과 어떤 룰인지 및 라인 위치를 표시해주고 있습니다.

잘 되는 것 같네요.

Git Hook을 적용하면서 기대했던 대로 동작하는 것 같습니다.

나만의 룰 만들기

한 동안은 SwiftLint에서 지원하고 있는 룰의 종류가 꽤 많기 때문에 기본 제공되는 룰만 사용해도 충분할 것 같았습니다.

그런데 코드 리뷰를 하다 보면 반복적으로 보이는 패턴이지만 SwiftLint에서 제공하는 룰 중에는 이 패턴을 잡을 수 없어서 아쉬웠던 적이 있었는데요.

SwiftLint는 이런 경우를 위해 정규표현식을 사용해서 직접 커스텀 룰을 추가할 수 있는 기능을 제공하고 있습니다.

커스텀 룰을 추가하기 위해 yaml파일 안에서 custom_rules 항목을 추가하고, regex: 부분에 정규표현식을 사용해서 커스텀 룰을 만들었습니다.

whitelist_rules:
- custom_rules


custom_rules:
    only_one_line_allowed_after_optional:
        name: 'Only One Line Allowed After Optional'
        regex: '(?:\!|\?)+$\n{3,}'
        message: '줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요.'
        severity: warning

잘 적용이 되는지 새로 추가한 룰을 적용하고 커밋을 해보았습니다.

SwiftLint Failed. Please check below:

  warning
    /Users/kimminho/baemin/HeaderView.swift:17:63
    Only One Line Allowed After Optional Violation - 줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요. (only_one_line_allowed_after_optional)

  warning
    /Users/kimminho/baemin/HeaderView.swift:21:63
    Only One Line Allowed After Optional Violation - 줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요. (only_one_line_allowed_after_optional)

  warning
    /Users/kimminho/baemin/HeaderView.swift:24:72
    Only One Line Allowed After Optional Violation - 줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요. (only_one_line_allowed_after_optional)

  warning
    /Users/kimminho/baemin/HeaderView.swift:30:52
    Only One Line Allowed After Optional Violation - 줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요. (only_one_line_allowed_after_optional)

  warning
    /Users/kimminho/baemin/HeaderView.swift:36:67
    Only One Line Allowed After Optional Violation - 줄 바꿈은 옵셔널로 선언 이후 한 줄까지만 해주세요. (only_one_line_allowed_after_optional)

Your commit is rejected. You have to fix it before committing again.

추가한 커스텀 룰도 잘 동작하는 것 같습니다.

커스텀 룰은 이름과 메시지를 직접 입력해야 하기 때문에 이렇게 한글로 써줘도 되니 보기도 편하네요.

마무리

지금까지 저희 앱서비스팀에서 코드 퀄리티를 높이기 위한 작업의 일환으로 코딩 스타일을 맞추는 작업을 진행한 경험에 대해서
얘기해봤는데요.

Git Hook을 적용하고 난 뒤 이전엔 코드 리뷰를 하면서 종종 언급 되던 코딩 스타일에 대한 얘기가 지금은 대부분 사라졌습니다.

그러면서 코드 리뷰의 내용도 기능 구현 위주로 바뀌어가면서 리뷰 퀄리티도 더 좋아진 것 같이 보입니다.

이번 경험을 통해 말씀드리고 싶은 부분은 저희 팀 처럼 여럿이 프로젝트를 공유하면서 각자의 코딩 스타일의 차이로 인해 고민한 경험이 있거나 코드 리뷰에 쓰는 시간을 조금이나마 줄여보고 싶다면 Git Hook을 이용해서 SwiftLint를 적용해보는 것도 한 가지 방법이 될 수 있을 것 같습니다.

참고