[CI] Next.js 프로젝트 세팅하기 (TypeScript + TailwindCSS + Prettier + ESLint + Husky + Commitlint + Commitizen)

0x45c·2024년 9월 1일
1

CI

목록 보기
2/5
post-thumbnail

새로운 사이드 프로젝트를 시작하면서 프로젝트 세팅을 하게 되었다.

이전의 프로젝트들도 계속 내가 세팅을 해오긴 했으나, 따로 정리한 문서가 없어서 나중에 다시 찾아도 볼겸.. 정리를 해보려고 한다.

이번 프로젝트는 Next.jsApp Router 방식을 사용하고 있으며, 커밋 컨벤션을 강제하기 위해 HuskyCommitizen을 함께 도입했다.

프로젝트 세팅에 익숙해지고자 매번 처음부터 새로 설정했었는데, 이번 이후로는 모노레포를 구축하여 동일한 설정을 가진 프로젝트를 쉽게 세팅할 수 있는 방식으로 전환하고자 한다.


🎯 목표

우선 여기에 나오는 세팅에서 목표하는 바는 크게 다음과 같다.

  1. ESLint와 Prettier 설정으로 일관된 코드 스타일 유지
  2. Husky와 lint-staged 설정으로 커밋 전 자동으로 코드 품질 검사를 수행하여 일관된 코드 품질을 유지
  3. Commitizen 설정으로 편리하게 커밋 컨벤션 지키기
  4. Commitlint: 일관된 커밋 메시지 형식을 강제하여 프로젝트 히스토리의 가독성 높이기 (git commit으로 직접 commit 하려는 경우에 대한 제한)

〰️ 흐름

커밋 시도 시 Husky가 pre-commit 훅을 실행하고, pre-commit 훅에서 lint-staged가 실행된다.

lint-staged는 변경된 파일에 대해 Prettier와 ESLint를 실행하게되고,
커밋 메시지 작성 시 Husky가 commit-msg 훅을 실행하여 Commitlint로 메시지 형식을 검사하는 흐름이다.

그리고 커밋 메시지 작성 시, Commitizen을 활용하여 보다 쉽게 컨벤션을 지키도록 했으며,
혹시라도 직접 git commit을 하는 경우에는 메시지에 강제 규칙을 넣어 컨벤션을 지키게 했다.


🛠️ 세팅 순서

1) 🚀 Create Next App

yarn create next-app {프로젝트 이름} --typescript

타입스크립트를 세팅하고, 나머지는 다음과 같이 설정했다.

2) 💅 Prettier, ESlint 설정

린팅으로는 기본적인 패키지만 포함시켰다.

yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser --dev
yarn add prettier-plugin-tailwindcss --dev
yarn add eslint-config-prettier --dev
yarn add eslint-plugin-prettier --dev
yarn add prettier --dev

.eslintrc.json 파일 생성:

{
  "extends": [
    "next/core-web-vitals",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier"
  ],
  "plugins": ["@typescript-eslint", "prettier"],
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "rules": {
    "prettier/prettier": "error",
    "react/react-in-jsx-scope": "off"
  }
}

.prettierrc 파일 생성:

*prettier 설정은 취향대로~

{
  "endOfLine": "lf",
  "printWidth": 100,
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "bracketSpacing": true,
  "bracketSameLine": true,
  "arrowParens": "always",
  "useTabs": false,
  "plugins": ["prettier-plugin-tailwindcss"]
}

package.json에 스크립트 추가:

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint './src/**/*.{ts,tsx,js,jsx}'",
  "lint:fix": "eslint --fix './src/**/*.{ts,tsx,js,jsx}'",
  "format": "prettier --check --ignore-path .gitignore .",
  "format:fix": "prettier --write --ignore-path .gitignore ."
}

🚨 주의: VSCode와 CLI 린팅 결과가 불일치 할 때

나같은 경우, yarn lint를 하면 린팅이 걸리는데, VSCode에서 실시간으로는 감지가 안되는 오류가 있었다.
찾아보니 VSCode의 settings.json 파일에서 ESLint 설정이 .eslintrc 파일을 참조하도록 되어있는 것을 확인했다. 이렇게 사용자마다 로컬 환경에서 특정 설정이 되어있는 경우를 커버하기 위해 다음과 같은 설정을 추가할 수 있다.

옵션: eslint 설정 파일 지정하기

프로젝트의 일관성을 유지하기 위해 VSCode의 ESLint 설정을 프로젝트 레벨에서 지정할 수 있는데, 이는 팀원들 간의 개발 환경 차이로 인한 문제를 예방하는 데 매우 유용하다.

다음 설정은 VSCode에게 프로젝트 루트의 .eslintrc.json 파일을 ESLint 구성 파일로 사용하도록 지정한다. 이를 통해 로컬 개발 환경 설정과 관계없이 모든 팀원이 동일한 ESLint 규칙을 적용받게 할 수 있다.

.vscode/settings.json 파일 생성:

{
  "eslint.options": {
    "overrideConfigFile": ".eslintrc.json"
  }
}

3) 🐶 Huksy 설정

yarn add -D husky
yarn add -D lint-staged
npx husky init

.husky/pre-commit 파일 생성:

#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

package.json에 추가:

"lint-staged": {
  "*.{js,jsx,ts,tsx}": [
    "prettier --write",
    "eslint --fix"
  ]
}

4) 📝 Commitlint 설정

yarn add @commitlint/config-conventional --dev
yarn add @commitlint/cli --dev

.husky/commit-msg 파일 생성:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}

.commitlintrc.json 파일 생성:
아래 설정을 통해 commit message의 규칙을 강제할 수 있다.
@commitlint/config-conventional 플러그인을 기본 룰로 사용했다.

자세한 사용법 참고) https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional

{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "type-enum": [
      2,
      "always",
      ["feat", "style", "chore", "fix", "ci", "refactor", "merge", "docs"]
    ],
    "type-case": [2, "always", "lower-case"],
    "type-empty": [2, "never"],
    "subject-empty": [2, "never"],
    "subject-case": [2, "never", []]
  }
}

package.json 에 스크립트 추가:

"scripts": {
  "prepare": "husky",
  "postinstall": "husky install"
}

5) 🤝 Commitizen 설정

1. 설치 및 기본 설정

yarn add -D commitizen

package.json 에 다음과 같이 추가 및 수정(commit-config.js 린팅 무시, commit 명령어 추가):

{
  "config": {
    "commitizen": {
      "path": "./commit-config.js"
    }
  },
  "scripts": {
    "lint": "eslint './src/**/*.{ts,tsx,js,jsx}' --ignore-pattern 'commit-config.js'",
    "lint:fix": "eslint --fix './src/**/*.{ts,tsx,js,jsx}' --ignore-pattern 'commit-config.js'",
    "commit": "./commitizen.sh"
  }
}

2. ESLint 설정 수정

.eslintrc.json 에 린팅 무시 파일 추가:

{
  "ignorePatterns": ["commit-config.js"]
}

1,2번에서 commit-config.js 파일에 대한 린팅을 무시하는 설정을 추가했다.
사실 ts로 수정하려다가 any 타입을 쓰거나 타입패키지를 추가하는 것 보다 그냥 개발 환경에 관련된 파일이니 린팅에서 무시하는게 나을거라고 판단했다. 이 부분은 나의 생각이 틀릴 수도 있으니, 취향에 맞게 설정하면 될 것 같다.

3. Commitlint 규칙 수정

.commitlintrc.json 파일 수정 (커밋 라벨 동일하게 맞춰주는 작업):

앞에 붙일 라벨들은 자유로 설정 가능!!

{
  "extends": ["@commitlint/config-conventional"],
  "rules": {
    "type-enum": [
      2,
      "always",
      [
        "✨ feat",
        "🐛 fix",
        "♻️ refactor",
        "🎨 design",
        "💎 style",
        "📦 chore",
        "💬 comment",
        "📚 docs",
        "🚑 !HOTFIX",
        "🚀 perf"
      ]
    ],
    "type-case": [2, "always", "lower-case"],
    "type-empty": [2, "never"],
    "subject-empty": [2, "never"],
    "subject-case": [2, "never", []],
    "header-max-length": [2, "always", 100]
  },
  "parserPreset": {
    "parserOpts": {
      "headerPattern": "^(\\S+):\\s(.+)\\s\\(#\\d+\\)$",
      "headerCorrespondence": ["type", "subject"]
    }
  }
}

4. Commitizen 실행 스크립트 생성

commitizen.sh 파일을 생성하고 다음 내용을 추가:

1. yarn commit으로 Commitizen을 통해 커밋하는 경우:

  • pre-commit 훅 실행 후 Commitizen 실행
  • commit-msg 훅은 건너뜀 (Commitizen이 형식을 보장하므로)

2. 일반 git commit으로 커밋하는 경우:

  • pre-commit 훅 실행
  • commit-msg 훅은 별도로 실행됨 (Husky에 의해 자동으로 처리)
#!/bin/sh

# Husky 환경 설정
. "$(dirname "$0")/.husky/_/husky.sh"

# pre-commit 훅 실행
if [ -f .husky/pre-commit ]; then
  .husky/pre-commit
else
  echo "Warning: .husky/pre-commit file not found. Skipping pre-commit hook."
fi

# pre-commit 훅이 성공적으로 실행되었다면 Commitizen 실행 (commit-msg 체크 건너 뜀)
# 직접 git commit 하는 경우가 있으니, 그때도 체크가 필요하기에 commit-msg 유지 필요
if [ $? -eq 0 ]; then
  HUSKY=0 cz
else
  echo "Pre-commit hook failed. Aborting commit."
  exit 1
fi

5. Commitizen 설정 파일 생성

commit-config.js 파일을 생성하고 다음 내용을 추가:

나는 라벨, 메시지, 이슈번호를 입력받아 다음과 같은 형태를 가지는 커밋 메시지 형태를 만들었다.

type: subject (#ticketNumber)

module.exports = {
  prompter: (cz, commit) => {
    const typeChoices = [
      { value: '✨ feat', name: '✨ feat:\t새로운 기능' },
      { value: '🐛 fix', name: '🐛 fix:\t버그 수정' },
      { value: '♻️ refactor', name: '♻️ refactor:\t코드 리팩토링' },
      { value: '🎨 design', name: '🎨 design:\tCSS 등 사용자 UI 디자인 변경' },
      {
        value: '💎 style',
        name: '💎 style:\t코드 포맷팅, 코드 변경이 없는 경우',
      },
      {
        value: '📦 chore',
        name: '📦 chore:\t빌드 업무 수정, 패키지 매니저 설정, 자잘한 코드 수정',
      },
      { value: '💬 comment', name: '💬 comment:\t주석 추가 및 변경' },
      { value: '📚 docs', name: '📚 docs:\t문서 수정' },
      {
        value: '🚑 !HOTFIX',
        name: '🚑 !HOTFIX:\t급하게 치명적인 버그를 고치는 경우',
      },
      { value: '🚀 perf', name: '🚀 perf:\t성능 개선' },
    ];

    const questions = [
      {
        type: 'list',
        name: 'type',
        message: '1️⃣  커밋 라벨을 선택하세요:',
        choices: typeChoices,
      },
      {
        type: 'input',
        name: 'subject',
        message: '2️⃣  커밋 메시지를 입력하세요:',
        validate: (input) => {
          if (input.length === 0) {
            return '커밋 메시지는 비워둘 수 없습니다.';
          }
          if (input.length > 100) {
            return '커밋 메시지는 100자를 넘을 수 없습니다.';
          }
          return true;
        },
      },
      {
        type: 'input',
        name: 'ticketNumber',
        message: '3️⃣  이슈 번호를 입력하세요 (숫자만):',
        validate: (input) => {
          if (!/^\d+$/.test(input)) {
            return '유효한 숫자를 입력해주세요.';
          }
          return true;
        },
      },
    ];

    cz.prompt(questions).then((answers) => {
      const { type, subject, ticketNumber } = answers;
      const message = `${type}: ${subject} (#${ticketNumber})`;

      const divider = '='.repeat(50);
      const decoratedMessage = `✅ 커밋 메시지가 다음과 같아요! 커밋할까요?
      ${divider}
      ${message}
      ${divider}
      `;
      // 확인 질문
      cz.prompt([
        {
          type: 'confirm',
          name: 'confirmCommit',
          message: decoratedMessage,
          default: false,
        },
      ]).then((confirmAnswer) => {
        if (confirmAnswer.confirmCommit) {
          commit(message);
        } else {
          console.log('❌ 커밋이 취소되었습니다.');
        }
      });
    });
  },
};

6. 실행 권한 부여

chmod +x .husky/pre-commit
chmod +x .husky/commit-msg
chmod +x commitizen.sh

팀원에게 공유할 때는, 다음 스크립트를 프로젝트에 추가하고,

update_permissions.sh 스크립트 추가:

#!/bin/bash

# Husky 스크립트에 실행 권한 부여
chmod +x .husky/pre-commit
chmod +x .husky/commit-msg

# commitizen.sh 스크립트에 실행 권한 부여
chmod +x commitizen.sh

echo "File permissions updated successfully."

다음 명령어를 실행하라고 하면 된다.
그리고 추가로 권한 부여가 필요하면 update_permissions.sh 내 스크립트만 수정하면 된다.

chmod +x update_permissions.sh
./update_permissions.sh

7. 실행 및 결과

yarn commit

1) yarn commit + 린팅 오류

  • pre-commit의 lint-staged에서 커밋 실패

2) yarn commit + 린팅 성공

  • pre-commit의 lint-staged 성공

3) git commit + 커밋 메시지 컨벤션 오류

  • commit-msg의 커밋 메시지 컨벤션에서 커밋 실패


다음은 github의 템플릿 설정과 CODEOWNER 설정을 통해 코드리뷰어 자동 세팅 하는 법을 정리해보려고 한다.

처음 프로젝트 세팅을 맡게 되었을 땐 막막했는데, 한두번 해보니 익숙해지고 재밌는 것 같다.

깊은 이해를 하려면 아직 멀었지만, 누군가에겐 프로젝트 세팅하는데 참고가 되었으면 좋겠다!

+추가로, 잘못된 내용이 있다면 알려주시면 감사하겠습니다!!!!

profile
열심히 배워가고 있는 3년차 프론트엔드 개발자입니다 :)

0개의 댓글