본문 바로가기

사이드 프로젝트

[클라이언트] 3. 프로젝트 초기 설정하기

1. 라이브러리 정하기

정말 대략적인 기획은 뽑았으니 나머지는 차차 해가는 걸로 합시다.

그렇다면 이제 프로젝트를 시작해봐야 합니다.

 

근데 문제가 있습니다.

회사 프로젝트만 하고 사이드 프로젝트를 잘 안 하다 보니까 뭐가 트렌드고 뭐가 좋은지를 잘 모르겠어요.

평소 공부하던 영상 외에 진짜 뭐가 괜찮은지 찾아보겠습니다.

 

일단 React로 시작하는 건 제가 그냥 정했습니다.

Angular는 언제 사라질까 무섭고, Vue는 제가 계속 개발을 유지한다는 가정 하에

프로젝트가 커질수록 좀 성능면 문제가 있지는 않을까 싶어서요.

다른 건 아직 생태계가 너무 작아서 제가 배우는데 더 오래 걸릴 것 같습니다.

 

React를 시작하는 방법은 크게 CRA, Webpack Vite, Next.js(SSR) 등이 있는데요.

CRA는 이제 지원도 중단되어 바로 버리고, Webpack은 ESBuilder에 비해 꽤 느립니다.

Next.js는 SSR을 지원하지만 제가 SSR이 필요하지는 않습니다.

저는 SEO가 크게 중요하지 않고, 돈이 없어서 좋은 서버를 둘 수 없어 서버 부하가 오면 큰 일 나거든요.

에디터다 보니까 사용자 인터렉션이 꽤 필요한 건 당연하고요.

 

그럼 Vite로 React를 시작하는 건 정했습니다.

이제 스타일을 정해야 하는데, 동적 스타일링이 좀 많을 것 같아 CSS In JS를 사용할까 합니다.

라이브러리가 많은데, style 태그를 계속 생성하는 다른 라이브러리에 비해 Emotion은 한 번만 생성하고

CSSOM에 직접 주입하여 성능 최적화가 꽤 잘 이루어져 있다고 하네요.

Emotion으로 해보겠습니다.

 

2. Vite로 React Typescript 프로젝트 시작하기

자 그럼 시작해 볼까요

제 프로젝트 이름은 AppEditor입니다.

 

npm create vite@latest AppEditor

위 명령어를 통해 vite로 프로젝트를 시작합니다.

 

그럼 진행하는 게 맞는지, 어떤 프로젝트를 시작할 건지, 언어는 뭔지를 설정할 수 있습니다.

저는 React 프로젝트, TypeScript+SWC를 선택했습니다.

 

여기서 SWC는 Speedy Web Compiler로 Rust 기반으로 작성된 JS 컴파일러라고 합니다.

Webpack이 Go 언어 기반으로 작성되어 성능이 빨라진 게 ESBuilder라면

기존 Babel이 Rust 언어 기반으로 작성되어 빠른 성능을 가지게 된 게 SWC라고 보시면 되겠습니다.

 

자 프로젝트를 시작했으면, 개발로 들어가기 전에 초기 설정을 좀 해줘야 합니다.

사용할 ESLint, 디렉터리 구조, 라이브러리, 코드 스타일 규칙을 미리 설정해 줘야

개발이 보다 빠르고 편리하게 진행될 수 있습니다.

 

3. ESlint 설정하기

ESlint는 JS 코드의 패턴을 맞춰주는 도구입니다.

JS의 다양한 사용성을 통해 코드 스타일이 일과되지 못하거나

문제 될 우려가 있는 코드의 사용을 제한하고 수정해 줄 수 있는 도구로 정적 코드 분석 도구이지만,

코드 스타일 유지에 도움을 주는 용도로 많이 사용됩니다.

 

ESlint 설정은 기본으로 되어있기는 하지만 저는 Toss에서 아이디어를 따온 김에 설정 양식도 좀 뺏어보겠습니다.

https://github.com/toss/slash에서 eslintrc.js 파일을 통해 쉽게 확인이 가능하네요. 

 

프로젝트 초기 eslint.config.js 파일

처음 프로젝트를 생성하면 위처럼 eslint가 설정되어 있을 텐데, 제 마음대로 아래처럼 작성해 주었습니다.

import globals from 'globals';
import typescriptParser from '@typescript-eslint/parser';
import typescriptESLint from '@typescript-eslint/eslint-plugin';
import reactHooksESLint from 'eslint-plugin-react-hooks';
import reactESLint from 'eslint-plugin-react';
import importESLint from 'eslint-plugin-import';

export default [
  {
    ignores: ['node_modules/', 'dist/'], // lint 적용 제외 경로
  },
  {
    files: ['**/*.{ts,tsx,js,jsx}'],
    languageOptions: {
      parser: typescriptParser,
      ecmaVersion: 2020,
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
      globals: globals.browser,
    },
    plugins: {
      '@typescript-eslint': typescriptESLint,
      react: reactESLint,
      import: importESLint,
      'react-hooks': reactHooksESLint,
    },
    settings: {
      'import/resolver': {
        typescript: {},
        node: {
          extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs'],
          moduleDirectory: ['node_modules', 'src/'],
        },
      },
      react: {
        version: 'detect',
      },
    },
    rules: {
      'no-implicit-coercion': 'error',
      'no-warning-comments': [
        'warn',
        {
          terms: ['TODO', 'FIXME', 'XXX', 'BUG'],
          location: 'anywhere',
        },
      ],
      curly: ['error', 'all'],
      eqeqeq: ['error', 'always', { null: 'ignore' }],

      // TypeScript-specific rules
      '@typescript-eslint/no-use-before-define': 'off',
      '@typescript-eslint/no-empty-interface': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-parameter-properties': 'off',
      '@typescript-eslint/no-var-requires': 'warn',
      '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
      '@typescript-eslint/no-inferrable-types': 'warn',
      '@typescript-eslint/no-empty-function': 'off',
      '@typescript-eslint/naming-convention': [
        'error',
        { format: ['camelCase', 'UPPER_CASE', 'PascalCase'], selector: 'variable', leadingUnderscore: 'allow' },
        { format: ['camelCase', 'PascalCase'], selector: 'function' },
        { format: ['PascalCase'], selector: 'interface' },
        { format: ['PascalCase'], selector: 'typeAlias' },
      ],
      '@typescript-eslint/explicit-module-boundary-types': 'off',
      '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
      '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
      '@typescript-eslint/member-ordering': [
        'error',
        {
          default: [
            'public-static-field',
            'private-static-field',
            'public-instance-field',
            'private-instance-field',
            'public-constructor',
            'private-constructor',
            'public-instance-method',
            'private-instance-method',
          ],
        },
      ],

      // React-specific rules
      'react/prop-types': 'off',
      'react/display-name': 'off',
      'react-hooks/exhaustive-deps': 'error',
      'react/react-in-jsx-scope': 'off',
      'react/no-unknown-property': ['error', { ignore: ['css'] }],
    },
  },
];

 

Toss Slash 라이브러리의 설정을 가져오면서 조금 변경해 주었습니다.

 

4. Prettier 설정하기

ESlint가 코드의 문법을 검토하면서 안정적인 코드 스타일을 지켜주는 도구였다면,

Prettier는 코드의 포맷을 일관되게 지켜주는 도구입니다.

 

들여 쓰기, 공백, 한 줄에 삽입 가능한 최대 코드 길이, 문장 규칙 등을 설정하여

여러 사람이 프로젝트를 작성해도 일관성 있게 양식을 갖춰주어 가독성을 높이는데 도움을 줍니다.

 

제가 작성할 Prettier는 아래와 같습니다.

export default {
  arrowParens: 'avoid',
  bracketSameLine: false,
  bracketSpacing: true,
  endOfLine: 'lf',
  jsxSingleQuote: false,
  printWidth: 120,
  proseWrap: 'preserve',
  quoteProps: 'as-needed',
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'es5',
};

이렇게 prettier.js를 작성해 두고 이걸 이제 ESlint와 함께 사용해서 서로 충돌을 없애고 잘 동작하도록 해보겠습니다.

 

먼저 아까 작성한 eslint.config.js 파일에 prettier와 관련된 eslint plugin과

방금 작성한 prettier.config.js 파일의 내용을 읽어오는 내용의 임포트를 추가합니다.

import prettierConfig from './prettier.config.js';
import prettierESLint from 'eslint-plugin-prettier';

 

이어서 아래에 plugins과 rules에 각각 추가해 주면 되는데 아래처럼 작성이 가능합니다.

plugins: {
  prettier: prettierESLint,
},
rules: {
  'prettier/prettier': ['error', prettierConfig]
}

 

 

5. Emotion 라이브러리 추가하기

Emotion 라이브러리는 공식 사이트를 통해 아래처럼 npm install을 해주면 추가가 가능합니다.

다만, jsx로 해석하라는 Pragma를 작성하라거나, ESlint가 문법을 오류로 인식하는 일이 발생해서 몇 설정이 필요합니다.

 

먼저 Pragma 제거를 위해 아래처럼 tsconfig.json과 vite.config.js를 수정해 줍시다.

tsconfig에 아래 내용을 추가하고,

"jsxImportSource": "@emotion/react"

vite.config.js의 plugins를 아래처럼 수정하면

plugins: [
    react({
      jsxImportSource: '@emotion/react',
    }),
    tsconfigPaths(),
  ],

이제부터 Pragma를 작성하지 않아도 emotion의 css를 사용하는 부분들을 알아서 jsx로 인식하고 빌드해 줍니다.

 

ESlint 설정은 또 아까 그 파일입니다.

eslint.config.js 파일에서 아래의 import, plugins, rules를 수정하면 됩니다.

import { rules as emotionESLint } from '@emotion/eslint-plugin';

plugins: {
	'@emotion': { rules: emotionESLint },
}
rules: {
	'@emotion/pkg-renaming': 'error',
}

 

6. Git Hooks 설정하기

Git Hooks는 Git의 특정 이벤트에 따라 미리 설정해 둔 스크립트를 실행할 수 있는 기능입니다.

npm 라이브러리 중 Husky라는 라이브러리는 이 Hooks 설정을 매우 편하게 도와주는 라이브러리이며,

저는 이걸 통해서 Hooks를 설정해 보겠습니다.

 

npm install --save-dev husky

위 명령어로 Husky 라이브러리를 package.json에 추가하고

npx husky init

명령어로 Husky를 시작하면, package.json에 prepare script가 husky라고 추가되고. husky라는 폴더가 생성됩니다.

이제 누구나 해당 레포지토리를 받아 시작할 때 자동으로 husky 내용이 추가됩니다.

 

저는 브랜치 맨 앞에 feat, chore 등 작업 내용을 알리고 커밋 메시지에 미리 삽입될 수 있는

간단한 커밋 컨벤션만 추가하려 합니다.

 

따라서 아래의 세 개 파일을 작성해 주었습니다.

 

commit-msg

#!/usr/bin/env sh

# 커밋 메시지 파일 경로
COMMIT_MSG_FILE=$1

# 커밋 메시지 파일에서 첫 번째 줄을 읽어옴
FIRST_LINE=$(head -n 1 "$COMMIT_MSG_FILE")

# 첫 번째 줄이 존재하고 길이가 10 이하인 경우, 에러 메시지 출력
if [ -n "$FIRST_LINE" ] && [ ${#FIRST_LINE} -le 10 ]; then
    echo 'Please write a valid title for your commit message.'
    exit 1
fi

# 정상적으로 커밋 메시지가 작성된 경우
exit 0

 

pre-commit

#!/usr/bin/env sh

# 현재 브랜치명 가져오기
BRANCH_NAME=$(git symbolic-ref --short HEAD)

# feat, chore, bugfix, refactor로 시작하는지 확인
if echo "$BRANCH_NAME" | grep -Eq '^(feat/|chore/|bugfix/|refactor/)'; then
  
  # 브랜치명이 규칙을 따를 경우 lint 실행
  npm run lint
  
  # lint가 실패하면 커밋 차단
  if [ $? -ne 0 ]; then
    echo "커밋 불가: lint 오류가 발생했습니다."
    exit 1
  fi
else
  echo "커밋 불가: 브랜치명은 feat, chore, bugfix, refactor 중 하나로 시작해야 합니다."
  exit 1
fi

 

prepare-commit-msg

#!/usr/bin/env sh

# 현재 브랜치명 가져오기
BRANCH_NAME=$(git symbolic-ref --short HEAD)

# 커밋 메시지 파일 경로
COMMIT_MSG_FILE=$1

# 브랜치명에서 '/' 이전 부분을 접두사로 추출
PREFIX=$(echo "$BRANCH_NAME" | cut -d'/' -f1)

# 접두사가 존재하고, 커밋 메시지가 해당 접두사로 시작하지 않는 경우 메시지 수정
if [ -n "$PREFIX" ]; then
  if ! grep -q "^₩[$PREFIX₩]" "$COMMIT_MSG_FILE"; then
    sed -i.bak "1s/^/[$PREFIX] /" "$COMMIT_MSG_FILE"
  fi
fi

 

 

7. 다음 이야기

드디어 프로젝트 초기 설정이 끝났습니다.

이제 npm run dev를 통해 프로젝트 실행이 가능하고 원하는 양식으로 바로 개발이 가능합니다.

 

제 레포지토리를 공개해서 좀 보여드리면 편하겠지만, 일단은 private 레포지토리로 진행하려 합니다.

제가 배포까지는 할 생각인데, 배포했을 때 잘되면 유료화도 좀 생각해보고 있거든요.

 

그래도, 코드의 일부들은 블로그에서 확인이 가능하니 필요하신 내용은 참고해서 구현하시면 될 것 같습니다.

다음 글에서는 디렉터리 구조 좀 나누고, 잡아둔 기획에 따라 뷰만 살짝 구현해 보겠습니다.

 

너무 길어졌는데 읽어주셔서 감사합니다.