*eslint는 airbnb typescript 세팅입니다.
*npm이 설치되어 있다는 가정하에 작성하였습니다.
*패키지 매니저는 npm과 pnpm 위주로 작성하였습니다.
*Next.js 는 13.4.19 버전
*storybook 7.3 버전
*lint-staged 14.0.1 버전
*husky 8.0.0 버전
*eslint 8.47.0 버전
*prettier 3.0.2 버전
*tailwindcss 3.3.3 버전
0. pnpm 설치(pnpm 설치되어 있으면 skip)
패키지 매니저 pnpm 사용 안 할거면 스킵.
npm install -g pnpm
pnpm 사용(명령어를 몇 개 정도만 정리해두었습니다.)
# 의존성 하나 설치할 때
pnpm add <packageName> // dependencies 에 저장
pnpm add -D <packageName> // devDependencies 에 저장
pnpm add -g <packageName> // 패키지를 전역으로 설치
기본적으로 최신 버전으로 새 패키지는 프로덕션 의존성으로 설치함. 워크스페이스에서 실행하는 경우,
명령은 먼저 워크스페이스에 있는 다른 프로젝트가 지정된 패키지를 사용하는지 여부를 확인합니다.
이 경우, 이미 사용된 버전이 설치됩니다.
# 이미 존재하는 프로젝트에서 pnpm-lock.yaml 파일에 입력되어 있는 모든 의존성 패키지들을 설치할 때 */
pnpm install
# pnpm에서 npm의 npx 명령어 대체하기
1. pnpx dlx : package를 registry에서 가져오고(없으면 다운로드) dependency로 설치하지는 않음. 그리고 패키지를 실행합니다.
2. pnpx (deprecated 되었으니 pnpx dlx를 사용하세요.)
3. pnpm create : create-* 형식의 이름을 가진 패키지를 사용하여 프로젝트를 생성하거나 혹은 @foo/create-* 과 같은 형식의 이름의 스타터 키트를 사용하여 생성할 때 씁니다.
# pnpx dlx와 pnpx
pnpm V7 이후로 pnpm dlx와 pnpx는 동일한 명령어가 되었음.
패키지를 가져오고 실행합니다.
pnpm dlx create-react-app my-app
# pnpm create
pnpm create react-app my-app // create-react-app 패키지를 다운로드.
# pnpm exec
패키지를 다운로드하지 않고 node_modules/.bin 에 이미 있는 패키지를 실행.
1. CNA (Create Next.js App) 설치
# npx
npx create-next-app@latest <my-new-app-name>
혹은 그냥
npx create-next-app@latest
# pnpm
pnpm create next-app@latest <my-new-app-name>
혹은 그냥
pnpm create next-app@latest
그러면 다음과 같은 prompt 문구들을 보게 됩니다.(기호에 따라 선택합시다.)
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes
What import alias would you like configured? @/*
기존 프로젝트(Next.js)에서 Next.ts로 생성하는 코드입니다.
touch tsconfig.json
pnpm install --save-dev @types/react @types/node
pnpm run dev # 잘 실행되면 됨 tsconfig.json에 자동 설정됨
*잠깐 설치되는 동안 읽을거리입니다.
Next.js에서는 Tailwind CSS를 공식적으로 밀어주고 있습니다.



Turbopack은 기존 webpack 의 후속작입니다. 새로운 번들러로서 Next.js 가 13 버전부터 도입한 번들러인데
webpack 대비 700 배 빠른 속도, webpack 대비 4배 빠른 초기 실행 속도, Vite 대비 2배가량 빠른 속도를 10배 빠른 업데이트 속도를 보여준다고 합니다.


tailwind를 next.js에서 쓰면 awesome 하겠습니다.
1-1. next 실행하기
/* npm */
npm run dev
/* pnpm run dev */
pnpm run dev
1-2. (간단한) 파싱 에러가 뜨는 경우
Parsing error: Cannot find module 'next/babel'
root 경로에 .babelrc 파일을 생성하고 다음과 같이 코드를 입력해 줍니다.
{
"presets": ["next/babel"],
"plugins": []
}
-> NextJS Project를 사용하고 있으면 추가적으로 .babelrc file을 생성하지 않아도 됩니다. 위 코드도 필요 없습니다. 하지 마세요.
그냥 .eslintrc.json 파일로 가서 다음과 같이 코드를 수정해주기만 하면 됩니다.
{
"extends": ["next/babel", "next/core-web-vitals"]
}
만약 "next/core-web-vitals" 을 지우고 "next/babel"만 남긴다면 에러는 사라지지만 컴파일 단계에서 컴파일을 하지 않고 컴파일 에러를 띄우므로 두 개 다 extends 설정에 넣어두도록 합시다.(next/babel 나중에 다시 뺄 것입니다. 에러는 골치 아프니 잠깐 잠재웁시다.)
1-3. corepack 사용하기(yarn 개발자가 만들었다고 함. 선택)
v.16.13부터 Node.js는 패키지 매니저를 관리하기 위해 corepack을 제공합니다. 아래 명령어를 실행하여 활성화해야 합니다.(관리자 권한 필요)
corepack enable
corepack 이 활성화되어 있는 상태에서 프로젝트의 package.json에 다음과 같이 설정되어 있으면
{
"packageManager": "yarn@2.4.3"
}
{
"packageManager": "npm@7.11.2"
}
해당 프로젝트에서 패키지매니저에 맞는 명령어만 사용할 수 있습니다.
npm 관리 설정(선택)
corepack의 default 설정은 yarn과 pnpm이 관리 대상이고 npm은 관리 대상이 아닙니다. 따라서 package manager가 yarn으로 설정된 프로젝트에서 npm 명령어를 사용해도 오류가 발생하지 않습니다. npm도 버전을 관리하기 위해서는 다음과 같이 설정해줘야 합니다.
$ corepack enable npm
default version 설정(선택)
프로젝트에 package manager에 대한 지정이 없는 경우 사용할 default version을 다음과 같이 설정할 수 있습니다.
아래 명령어를 사용하면 시스템에 pnpm이 자동으로 설치됩니다. 그러나 아마 최신 버전 pnpm이 아닐 수도 있습니다.
업그레이드하려면 최신 pnpm 버전이 무엇인지 확인해야 합니다.
# version에 특정 버전 기입
corepack prepare pnpm@<version> --activate
고민하지 않고 최신 버전 설치하려면 위 명령어가 아닌 아래의 다음 명령어를 사용합시다.
corepack prepare pnpm@latest --activate
2. ESLint 설정
Next.js 버전 11.0.0부터 ESLint가 함께 통합됐기 때문에 Next 설치 때 eslint 설치에 yes 했다면, ESLint를 추가 설치하거나
.eslintrc.json 파일을 따로 생성하지 않아도 됩니다.
- eslint에는 airbnb, google, next 등 다양한 규칙들이 있습니다. 그중에서 대표적인 airbnb 규칙을 사용해서 설치해 보겠습니다.
2-1. Airbnb 규칙 설정
airbnb + 종속 패키지 설치
/*eslint-config-aribnb의 의존성 패키지 목록을 확인할 수 있는 명령어 */
// 무엇을 설치해야 하는 지 더 정확하게 알 수 있다.
/* npm */
npm info "eslint-config-airbnb@latest" peerDependencies
/* pnpm */
pnpm info "eslint-config-airbnb@latest" peerDependencies
/* airbnb + 종속 패키지까지 설치하기 */
/* npm */
npx install-peerdeps --dev eslint-config-airbnb
// 또는 각각 개별적 설치
npm install -D eslint-config-airbnb eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
/* pnpm */
// pnpm은 자동적으로 peer dependencies를 올바르게 더 나은 방향으로 해결해주기 때문에
// install-peerdeps와 같은 분리된 커맨드가 필요없습니다.
// pnpm add 명령어를 쓸 때 peer dependencies까지 자동으로 고려를 해서 설치를 해줍니다.
pnpm add --save-dev eslint-config-airbnb
(--save-dev 대신 -D 가능. 맨 위 카테고리 커멘드 관련 링크 참고할 것)
// 또는 각각 개별적 설치
pnpm add -D eslint-config-airbnb eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
pnpm으로 설치 시 어떻게 될까? (처음 사용)
'종속 패키지들이 devDependencies 에는 없는데 어디에 있는 걸까?'라고 생각하고 궁금해서 찾아봤는데요? node_modules랑 pnpm-lock.yaml에 있었습니다.
pnpm 과 .npmrc도 따로 공부를 하고 어떻게 해야 효율적으로 사용할 수 있을지 기록해 놔야겠습니다.

프로젝트 내의 pnpm-lock.yaml 파일을 보면 다음과 같이 settings가 있습니다.
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false


TypeScript 관련 airbnb, eslint 패키지 설치
/* npm */
npm install -D eslint-config-airbnb-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser
/* pnpm */
pnpm add -D eslint-config-airbnb-typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser
2-2. eslint-config-airbnb-base 리액트 제외 규칙 (선택)
- 리액트를 안 쓰는 경우 설치합니다. (위에서 다룬 airbnb 설치 내용은 react에 적용시키는 버전입니다.)
- 종속성 패키지 확인 명령어
/* npm */
npm info "eslint-config-airbnb-base@latest" peerDependencies
/* pnpm */
pnpm info "eslint-config-airbnb-base@latest" peerDependencies
설치
/* npm */
npx install-peerdeps --dev eslint-config-airbnb-base
/* pnpm */
pnpm add -D --save-peer eslint-config-airbnb-base
.eslintrc.json 파일에 설정 추가
"env": {
"browser": true,
"node": true,
},
"extends": "airbnb-base",
"rules": {
"linebreak-style": 0,
},
2-3. .eslintrc.json 파일 세팅
{
"root": true,
// 보통 js워크스페이스에서는 @babel/eslint-parser를 사용하고 ts워크스페이스인 경우 @typescript-eslint/parser를 사용한다.
// plugin:@typescript-eslint/recommended를 포함시키면 @typescript-eslint/parser가 자동으로 포함된다.
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier", "react", "react-hooks", "jsx-a11y", "import"],
"parserOptions": {
"project": "./tsconfig.json",
"createDefaultProgram": true
},
"env": { // 전역객체를 eslint가 인식하는 구간
"browser": true, // document나 window 인식되게 함
"node": true,
"es6": true
},
"ignorePatterns": ["node_modules/"], // eslint 미적용될 폴더나 파일 명시
"extends": [
"airbnb",
"airbnb-typescript",
"airbnb/hooks",
"plugin:@typescript-eslint/recommended", // ts 권장
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:storybook/recommended",
"prettier", // eslint-config-prettier prettier와 중복된 eslint 규칙 제거
"plugin:jsx-a11y/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:prettier/recommended", // (eslint-plugin-prettier) eslint의 포매팅을 prettier로 사용. 항상 마지막에 세팅 되어야 함.
],
"rules": {
"react/react-in-jsx-scope": "off", // react 17부턴 import 안해도돼서 기능 끔
"react/jsx-props-no-spreading": "off",
"no-console": "off",
"no-var": "error",
// 경고표시, 파일 확장자를 .ts나 .tsx 모두 허용함
"react/jsx-filename-extension": ["warn", { "extensions": [".ts", ".tsx"] }],
"no-useless-catch": "off" // 불필요한 catch 못쓰게 하는 기능 끔
}
}
참고로 각 프로퍼티의 값인 "off ", "warn", "error" 옵션 값은 0, 1, 2 숫자에 매핑되어 있습니다. 숫자로 입력해 놓아도 된다는 뜻입니다.
2-4. eslint 부록 (절대경로, tsconfig에 맞춰서 세팅, import 순서 맞추기 세팅, 최종 세팅)
import 순서 맞추기 세팅
tsconfig에서 경로의 단순화를 위해서 custom alias 세팅을 할 때가 있습니다. 다음과 같이 말이죠.
/* tsconfig.json */
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
// "@/lib/utils/*": ["./src/lib/utils/*"],
},
}
...
}
이 때 eslint에서 import를 그룹화하고 순서를 맞춰서 import 하도록 규칙을 강제할 수 있습니다.
아래 파일은 .eslintrc.cjs 파일입니다만 .eslintrc.json 파일이어도 상관이 없습니다.
어떤 역할을 하는지는 주석을 통해 확인해주세요.
module.exports = {
// packagemanager pnpm임. pnpm run lint --fix src 실행
root: true,
env: {
// 전역객체를 eslint가 인식하는 구간
// node 에서 돌리는 파일이면 browser 대신 node를 설정.(나중에 하나만 선택하자)
browser: true,
es2020: true,
node: true,
},
extends: [
'airbnb',
'airbnb-typescript',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended', // ts 권장
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'prettier', // eslint-config-prettier prettier와 중복된 eslint 규칙 제거
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:prettier/recommended', // (eslint-plugin-prettier) eslint의 포매팅을 prettier로 사용. 항상 마지막에 세팅 되어야 함.
],
ignorePatterns: [
'dist',
'.eslintrc.cjs',
'vite.config.ts',
'node_modules/',
'postcss.config.js',
'tailwind.config.ts',
'__previewjs__/*',
'tailwindcss-extend.cjs',
],
parser: '@typescript-eslint/parser',
// # 참고 https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts#expanding-the-eslint-configuration
parserOptions: {
project: ['./tsconfig.json', './tsconfig.node.json'],
createDefaultProgram: true,
},
plugins: ['@typescript-eslint', 'prettier', 'react', 'react-hooks', 'jsx-a11y', 'import'],
rules: {
'react/react-in-jsx-scope': 'off', // react 17부턴 상단에 "react" import 안 해도 돼서 기능 끔
'react/jsx-props-no-spreading': 'off',
'no-console': 'off',
'no-var': 'error',
// 경고표시, 파일 확장자를 .ts나 .tsx 모두 허용함
'react/jsx-filename-extension': ['warn', { extensions: ['.ts', '.tsx'] }],
'no-useless-catch': 'off', // 불필요한 catch 못쓰게 하는 기능 끔,
'@typescript-eslint/no-unused-vars': 'warn',
'react/function-component-definition': 'off',
'jsx-a11y/mouse-events-have-key-events': 'off',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-curly-brace-presence': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'import/prefer-default-export': 'off',
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'warn',
'no-return-assign': 'off',
'no-param-reassign': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'react/require-default-props': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/return-await': 'off',
'react/no-unescaped-entities': [
'warn',
{
forbid: [
{
char: '>',
alternatives: ['>'],
},
{
char: '·',
alternatives: ['·'],
},
{ char: "'", alternatives: ['''] },
{ char: '“', alternatives: ['"'] },
{ char: '”', alternatives: ['"'] },
{ char: '•', alternatives: ['•'] },
{ chat: '©', alternatives: ['©'] },
],
},
],
'import/extensions': [
'error',
'ignorePackages',
{
'': 'never',
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': [
'error',
{
// devDependencies 라이브러리에서 모듈을 import 해서 쓰는 것을 허락.
devDependencies: true,
},
],
// # 경로간 import 순서를 규정
'import/order': [
'error',
{
// # 공식링크: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md
// # 참고링크: https://db2dev.tistory.com/entry/ESLint-importorder-%EA%B7%9C%EC%B9%99-%EC%84%A4%EC%A0%95%ED%95%98%EA%B3%A0-%EB%92%A4%EC%A3%BD%EB%B0%95%EC%A3%BD-import-%EC%BD%94%EB%93%9C-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0
// * 大그룹 순서 규칙을 정의한다. []배열을 통해 그룹 간 mingle을 할 수 있다.
// ! internal group은 따로 pattern으로 지정해야 생김. 경로가 없으면(패키지) external group로 인식한다.
// parent 경로와 sibling 경로끼리는 순서가 서로 뒤섞일 수 있게 함.(먼저 오는 게 승리) index는 전체 폴더에서 현재 폴더의 순서인 것 같음.
// The default value is ["builtin", "external", "parent", "sibling", "index"].
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'type', 'unknown'],
pathGroups: [
{
// * 이 pattern에 매칭되면 group에 명시된 그룹 소속이 된다.(필수옵션 아님)
// ! group에 명시된 그룹 소속이 되나, import 구문을 보면 그룹 중에서도 pattern 적용시킨 그룹끼리만 묶인다.("newlines-between": "always"로 확인 가능.)
// ? builtins 와 externals 에는 적용되지 않기 때문에(다른 그룹으로 안 바뀜) pathGroupsExcludedImportTypes에서 명시를 해줘야 한다. => 잘 모르겠음. 정확하지 않음.
// pattern 표현식 참고 링크 https://getithub.com/motemen/minimatch-cheat-sheet#braces
pattern: '{react*,react*/**,react-dom/**,@tanstack/react-query/**}',
// * 적용될 그룹 타입.
group: 'external',
// * 적용될 그룹 내에서 배치될 위치. after 또는 before
// 위의 매치된 pattern을 위의 group 내에서 가장 앞에 위치 시킨다. -> before
// 생략할 경우 앞, 뒤가 아닌 그룹과 같이 배치됨.
// before는 앞 / after는 뒤
// position이 같을 경우 pathGroups에서 인덱스가 앞서있는 pattern이 우선순위.
position: 'before',
},
// ! tsconfig에서 path를 configure 한 것(custom directory alias)은 external group으로 다뤄진다. -> 필요시 직접 internal group으로 지정해줘야 함.
{
pattern: '@/{pages,components,assets}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/{lib,utils,hooks,event}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/{store,slice}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/pages/**/*.style',
group: 'unknown',
},
{
pattern: '@/components/**/*.style',
group: 'unknown',
},
{
pattern: '{.,..}/**/*.style',
group: 'unknown',
},
{
pattern: '*.style',
group: 'unknown',
},
{
pattern: 'tw*.ts',
group: 'unknown',
},
],
// todo: 이건 진짜 정확하게 모르겠다. 공부하자. -> pathGroups에 명시된 pattern에서 제외시킬 명단?을 적는다.
// 참고링크:https://tesseractjh.tistory.com/305
// ?(Array 패턴) 적힌 디렉토리 또는 패키지를 pathGroups에서 제외시키나?
// pathGroupsExcludedImportTypes에 명시된 라이브러리는 pathGroups에서 매칭된 패턴에서 벗어날 수 있다.
// 예를 들어 "pattern": "react*", 이 있을 때, pathGroupsExcludedImportTypes에 "react"를 적어주면 react는 react*(react, react-dom 등)과 분리하여 다른 외부 라이브러리와 동일한 우선순위를 갖는다.
// 설정된 pathGroups으로 인해서 처리되지 않는 import type(보통 external group, external import 처럼 보이는 path Groups)을 정의
pathGroupsExcludedImportTypes: ['react'],
// * 새로운 import 그룹 간에 줄바꿈을 강요할지 금지할 지 정한다. -> 기본값 ignore
// ! 그룹 중에서도 pattern을 통해 그룹핑이 된 그룹이 있으면 그 사이에 또 newline이 생긴다.
// ? newlines-between:always는 vscode focusOnChange save만으로는 auto-fix 기능이 안 된다. 왜 안되는지 모르겠다. 이것만 안된다. 직접 ctrl+s 눌렀을 때만 auto-fix된다.
// newlines-between: [ignore|always|always-and-inside-groups|never]
'newlines-between': 'always',
// * 알파벳 정렬 규칙 정의
alphabetize: {
// * 오름차순 -> asc / 내림차순 -> desc / default -> ignore
order: 'asc',
// * 대소문자 무시하고 전부 소문자로 취급하려면 -> true / 대소문자 따지려면 -> default false
caseInsensitive: true,
},
// * 등록되지 않은 import의 순서가 잘못됐을 경우 경고 발생 여부(웬만하면 안 쓰는 게 좋음.)
// 등록되지 않은 import 순서가 잘못됐을 경우 오류 -> default false
// error가 아닌 warn으로 바꾸기 때문에 lint --fix 명령어가 수정하지 않음. -> true
// "warnOnUnassignedImports": false
},
],
// # 공식링크: https://eslint.org/docs/latest/rules/sort-imports#options
// # 모듈 간 import 순서를 규정
'sort-imports': [
'error',
{
// * 가져온 모듈의 대소문자 구분을 무시. 다 소문자로 취급하고 정렬. => true / 대문자가 앞에 와야함 => 기본값 false
ignoreCase: true,
// * import 하려는 모듈의 선언 순서 정렬을 무시할지 말지 결정.
// 가져오려는 모듈 이름의 알파벳 순으로 정렬 => 기본값 false
// 알파벳 순서 정렬 무시 => true
ignoreDeclarationSort: true,
// * 같은 파일에서 가져오는 모듈들도 순서를 맞춰야 함. -> default false ex) import { a, b, c } from './utils';
// 순서 안 맞춰도 됨 -> true
ignoreMemberSort: false,
// * 모듈 간 import 한 줄 띄어쓰기 허용 여부(import/order의 newlines-between와 충돌이 일어나지 않게 그때그때 맞춰서 설정한다.)
// 모듈 import 한 줄 띄어쓰기 불가능 => 기본 false
// 모듈 import 그룹처럼 묶을 수 있고, 그룹간 한 줄 띄어쓰기가 가능. 연속적인 import끼리만 순서를 따지고 한 줄 띄어쓰기 이후부터는 새로 따짐. => true
allowSeparatedGroups: true,
},
],
},
};
Unable to resolve path to module ‘@page/Login’ eslint(import/no-unresolved) 에러
위처럼 세팅했을 때 모듈을 import 한 부분에서 아래와 같은 에러 문구가 뜰 것입니다.
Unable to resolve path to module ‘@page/Login’ eslint(import/no-unresolved)
이를 해결하기 위해서 "eslint-import-resolver-typescript" 를 설치해줍니다.
/* npm */
npm i -D eslint-import-resolver-typescript
/* pnpm */
pnpm add -D eslint-import-resolver-typescript
그리고 eslint 파일을 다음과 같은 코드를 추가해줍니다.
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
},
},
에러가 사라질 것입니다. 에러가 사라지지 않는다면 리로드를 해주세요.
eslint 최종 세팅 모습
저는 최종 세팅을 했을 때 다음과 같은 모습이 됐습니다.
.eslintrc.cjs 파일
module.exports = {
// packagemanager pnpm임. pnpm run lint --fix src 실행
root: true,
env: {
// 전역객체를 eslint가 인식하는 구간
// node 에서 돌리는 파일이면 browser 대신 node를 설정.(나중에 하나만 선택하자)
browser: true,
es2020: true,
node: true,
},
extends: [
'airbnb',
'airbnb-typescript',
'airbnb/hooks',
'plugin:@typescript-eslint/recommended', // ts 권장
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'prettier', // eslint-config-prettier prettier와 중복된 eslint 규칙 제거
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:prettier/recommended', // (eslint-plugin-prettier) eslint의 포매팅을 prettier로 사용. 항상 마지막에 세팅 되어야 함.
],
ignorePatterns: [
'dist',
'.eslintrc.cjs',
'vite.config.ts',
'node_modules/',
'postcss.config.js',
'tailwind.config.ts',
'__previewjs__/*',
'tailwindcss-extend.cjs',
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {},
},
// "tailwindcss": {
// "callees": ["cn"]
// }
},
parser: '@typescript-eslint/parser',
// # 참고 https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts#expanding-the-eslint-configuration
parserOptions: {
project: ['./tsconfig.json', './tsconfig.node.json'],
createDefaultProgram: true,
// ! 🚨 sourceType부터 tsconfigRootDir까지는 원래 없었음. 마지막에 추가.(eslint 제대로 안 돌아가면 여기가 문제임!!)
sourceType: 'module',
ecmaVersion: 'latest',
tsconfigRootDir: __dirname,
// "ecmaVersion": 2020
},
plugins: ['@typescript-eslint', 'prettier', 'react', 'react-hooks', 'jsx-a11y', 'import'],
rules: {
'react/react-in-jsx-scope': 'off', // react 17부턴 상단에 "react" import 안 해도 돼서 기능 끔
'react/jsx-props-no-spreading': 'off',
'no-console': 'off',
'no-var': 'error',
// 경고표시, 파일 확장자를 .ts나 .tsx 모두 허용함
'react/jsx-filename-extension': ['warn', { extensions: ['.ts', '.tsx'] }],
'no-useless-catch': 'off', // 불필요한 catch 못쓰게 하는 기능 끔,
'@typescript-eslint/no-unused-vars': 'warn',
'react/function-component-definition': 'off',
'jsx-a11y/mouse-events-have-key-events': 'off',
'react/jsx-no-useless-fragment': 'off',
'react/jsx-curly-brace-presence': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'import/prefer-default-export': 'off',
'react-hooks/rules-of-hooks': 'off',
'react-hooks/exhaustive-deps': 'warn',
'no-return-assign': 'off',
'no-param-reassign': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
'react/require-default-props': 'off',
'@typescript-eslint/no-shadow': 'off',
'@typescript-eslint/return-await': 'off',
'react/no-unescaped-entities': [
'warn',
{
forbid: [
{
char: '>',
alternatives: ['>'],
},
{
char: '·',
alternatives: ['·'],
},
{ char: "'", alternatives: ['''] },
{ char: '“', alternatives: ['"'] },
{ char: '”', alternatives: ['"'] },
{ char: '•', alternatives: ['•'] },
{ chat: '©', alternatives: ['©'] },
],
},
],
'import/extensions': [
'error',
'ignorePackages',
{
'': 'never',
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': [
'error',
{
// devDependencies 라이브러리에서 모듈을 import 해서 쓰는 것을 허락.
devDependencies: true,
},
],
// # 경로간 import 순서를 규정
'import/order': [
'error',
{
// # 공식링크: https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md
// # 참고링크: https://db2dev.tistory.com/entry/ESLint-importorder-%EA%B7%9C%EC%B9%99-%EC%84%A4%EC%A0%95%ED%95%98%EA%B3%A0-%EB%92%A4%EC%A3%BD%EB%B0%95%EC%A3%BD-import-%EC%BD%94%EB%93%9C-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0
// * 大그룹 순서 규칙을 정의한다. []배열을 통해 그룹 간 mingle을 할 수 있다.
// ! internal group은 따로 pattern으로 지정해야 생김. 경로가 없으면(패키지) external group로 인식한다.
// parent 경로와 sibling 경로끼리는 순서가 서로 뒤섞일 수 있게 함.(먼저 오는 게 승리) index는 전체 폴더에서 현재 폴더의 순서인 것 같음.
// The default value is ["builtin", "external", "parent", "sibling", "index"].
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'type', 'unknown'],
pathGroups: [
{
// * 이 pattern에 매칭되면 group에 명시된 그룹 소속이 된다.(필수옵션 아님)
// ! group에 명시된 그룹 소속이 되나, import 구문을 보면 그룹 중에서도 pattern 적용시킨 그룹끼리만 묶인다.("newlines-between": "always"로 확인 가능.)
// ? builtins 와 externals 에는 적용되지 않기 때문에(다른 그룹으로 안 바뀜) pathGroupsExcludedImportTypes에서 명시를 해줘야 한다. => 잘 모르겠음. 정확하지 않음.
// pattern 표현식 참고 링크 https://getithub.com/motemen/minimatch-cheat-sheet#braces
pattern: '{react*,react*/**,react-dom/**,@tanstack/react-query/**}',
// * 적용될 그룹 타입.
group: 'external',
// * 적용될 그룹 내에서 배치될 위치. after 또는 before
// 위의 매치된 pattern을 위의 group 내에서 가장 앞에 위치 시킨다. -> before
// 생략할 경우 앞, 뒤가 아닌 그룹과 같이 배치됨.
// before는 앞 / after는 뒤
// position이 같을 경우 pathGroups에서 인덱스가 앞서있는 pattern이 우선순위.
position: 'before',
},
// ! tsconfig에서 path를 configure 한 것(custom directory alias)은 external group으로 다뤄진다. -> 필요시 직접 internal group으로 지정해줘야 함.
{
pattern: '@/{pages,components,assets}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/{lib,utils,hooks,event}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/{store,slice}/**/*',
group: 'internal',
position: 'after',
},
{
pattern: '@/pages/**/*.style',
group: 'unknown',
},
{
pattern: '@/components/**/*.style',
group: 'unknown',
},
{
pattern: '{.,..}/**/*.style',
group: 'unknown',
},
{
pattern: '*.style',
group: 'unknown',
},
{
pattern: 'tw*.ts',
group: 'unknown',
},
],
// todo: 이건 진짜 정확하게 모르겠다. 공부하자. -> pathGroups에 명시된 pattern에서 제외시킬 명단?을 적는다.
// 참고링크:https://tesseractjh.tistory.com/305
// ?(Array 패턴) 적힌 디렉토리 또는 패키지를 pathGroups에서 제외시키나?
// pathGroupsExcludedImportTypes에 명시된 라이브러리는 pathGroups에서 매칭된 패턴에서 벗어날 수 있다.
// 예를 들어 "pattern": "react*", 이 있을 때, pathGroupsExcludedImportTypes에 "react"를 적어주면 react는 react*(react, react-dom 등)과 분리하여 다른 외부 라이브러리와 동일한 우선순위를 갖는다.
// 설정된 pathGroups으로 인해서 처리되지 않는 import type(보통 external group, external import 처럼 보이는 path Groups)을 정의
pathGroupsExcludedImportTypes: ['react'],
// * 새로운 import 그룹 간에 줄바꿈을 강요할지 금지할 지 정한다. -> 기본값 ignore
// ! 그룹 중에서도 pattern을 통해 그룹핑이 된 그룹이 있으면 그 사이에 또 newline이 생긴다.
// ? newlines-between:always는 vscode focusOnChange save만으로는 auto-fix 기능이 안 된다. 왜 안되는지 모르겠다. 이것만 안된다. 직접 ctrl+s 눌렀을 때만 auto-fix된다.
// newlines-between: [ignore|always|always-and-inside-groups|never]
'newlines-between': 'always',
// * 알파벳 정렬 규칙 정의
alphabetize: {
// * 오름차순 -> asc / 내림차순 -> desc / default -> ignore
order: 'asc',
// * 대소문자 무시하고 전부 소문자로 취급하려면 -> true / 대소문자 따지려면 -> default false
caseInsensitive: true,
},
// * 등록되지 않은 import의 순서가 잘못됐을 경우 경고 발생 여부(웬만하면 안 쓰는 게 좋음.)
// 등록되지 않은 import 순서가 잘못됐을 경우 오류 -> default false
// error가 아닌 warn으로 바꾸기 때문에 lint --fix 명령어가 수정하지 않음. -> true
// "warnOnUnassignedImports": false
},
],
// # 공식링크: https://eslint.org/docs/latest/rules/sort-imports#options
// # 모듈 간 import 순서를 규정
'sort-imports': [
'error',
{
// * 가져온 모듈의 대소문자 구분을 무시. 다 소문자로 취급하고 정렬. => true / 대문자가 앞에 와야함 => 기본값 false
ignoreCase: true,
// * import 하려는 모듈의 선언 순서 정렬을 무시할지 말지 결정.
// 가져오려는 모듈 이름의 알파벳 순으로 정렬 => 기본값 false
// 알파벳 순서 정렬 무시 => true
ignoreDeclarationSort: true,
// * 같은 파일에서 가져오는 모듈들도 순서를 맞춰야 함. -> default false ex) import { a, b, c } from './utils';
// 순서 안 맞춰도 됨 -> true
ignoreMemberSort: false,
// * 모듈 간 import 한 줄 띄어쓰기 허용 여부(import/order의 newlines-between와 충돌이 일어나지 않게 그때그때 맞춰서 설정한다.)
// 모듈 import 한 줄 띄어쓰기 불가능 => 기본 false
// 모듈 import 그룹처럼 묶을 수 있고, 그룹간 한 줄 띄어쓰기가 가능. 연속적인 import끼리만 순서를 따지고 한 줄 띄어쓰기 이후부터는 새로 따짐. => true
allowSeparatedGroups: true,
},
],
},
};
3. prettier 설치
3-1. prettier 및 플러그인 설치
eslint와 prettier를 함께 사용할 시 규칙들이 충돌되므로 의존성 패키지들을 설치해야 합니다.
/* npm */
npm i -D prettier eslint-plugin-prettier eslint-config-prettier
/* pnpm */
pnpm add -D prettier eslint-plugin-prettier eslint-config-prettier
- eslint-plugin-prettier : eslint에서 prettier랑 충돌할 규칙 비활성화
- eslint-config-prettier : 포매팅할 때 prettier 사용하게 하기
이제 vscode settings.json 파일도 만들어서 공유할 수 있도록 합니다.
workSpace settings 예시(.vscode/settings.json 파일)
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// VSCode에 내장되어 있는 자바스크립트 포맷팅 기능을 사용하지 않고 Prettier 익스텐션을 사용하기 위해서 설정.
"[javascript]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// ESLint에 의한 자동 수정 기능을 활성화하기 위해서 설정
"editor.codeActionsOnSave": {
// For ESlint
"source.fixAll.eslint": true
},
// 에디터 바깥으로 포커스가 이동 시 파일을 자동으로 저장하기 위해서 설정
"files.autoSave": "onFocusChange",
/* tailwind unknown error resolve */
"css.lint.unknownAtRules": "ignore"
}
3-2. .prettierrc.cjs 파일 생성 및 세팅
원하는 prettier 옵션을 적어줍니다.(주석 참고)
module.exports = {
// doubleQuote: true,
// 문자열은 singleQuote로 ("" -> '')
singleQuote: true,
jsxSingleQuote: true,
//코드 마지막에 세미콜론이 있게 formatting
semi: true,
// 들여쓰기 너비는 2칸
tabWidth: 2,
// 배열 키:값 뒤에 항상 콤마를 붙히도록 formatting
trailingComma: 'all',
// 코드 한줄이 maximum 80칸
printWidth: 80,
// 화살표 함수가 하나의 매개변수를 받을 때 괄호를 생략하게 formatting
arrowParens: 'avoid',
// windows에 뜨는 'Delete cr' 에러 해결
endOfLine: 'auto',
bracketSpacing: true, // 중괄호 내에 공백 사용,
// 주석에서 '//' 다음에 예외 블록, 공백 또는 탭이 오도록 설정
// Expected exception block, space or tab after '//' in comment.eslint
// 에러를 해결하기 위한 설정입니다.
// 원하는 대로 예외 블록, 공백 또는 탭을 지정해 주세요.
// 예를 들어, 'always'로 설정하면 '//' 다음에 공백이 필요합니다.
// 'never'로 설정하면 '//' 다음에 공백이 오면 에러가 발생합니다.
// commentLineExceptions: ['always'],
// annotationSpacing: 'always',
};
.prettierignore 파일 세팅(선택)
prettier 포매팅을 적용시키지 않을 파일들이 있다면 이 파일 안에 적어줍니다.
/* .prettierignore */
// By default prettier ignores files in version control systems directories (".git", ".svn" and ".hg") and node_modules (if --with-node-modules CLI option not specified)
# .prettierignore 파일 안에 포맷팅을 하지 않을 파일들을 넣어준다.
build
coverage
혹은 컴포넌트단에서 prettier를 적용시키지 않는 방법도 있습니다.
/* Javascript */
matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)
// prettier-ignore
matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)
// will be transformed to:
matrix(1, 0, 0, 0, 1, 0, 0, 0, 1);
// prettier-ignore
matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)
/* JSX */
<div>
{/* prettier-ignore */}
<span ugly format='' />
</div>
4. prettier, eslint 실행
prettier(코드 스타일 자동정리) -> eslint(규칙검사) -> 코드 수정 순서로 진행됩니다.
prettier 실행
--write 대신 --check flag를 주게 되면 formatting 하기 전에 이미 formatting 된 것인지 확인할 수 있다고 합니다.
선택사항으로 프로젝트 때 하시길 권장드립니다. 곧장 --write 하는 것보다는 이미 포매팅이 되었는지 --check 후에 --write를 해주면 되겠습니다.(선택)
app 폴더 밑의 파일들을 prettier로 코드 스타일을 자동 정리합니다.
/* npm */
npx prettier --write app
/* pnpm */
pnpm exec prettier --write app
eslint 실행
/* npm */
npm run lint
/* pnpm */
pnpm run lint
에러가 날 때 굳이 사용하고 싶지 않은 규칙이 있다면 해당 속성을 .eslintrc.json 파일에 적어줍니다.
5. husky와 lint-staged로 편리하게 사용하기
husky와 lint-staged를 사용하여 git commit 할 때 변경된 파일만 eslint, prettier 자동 실행할 수 있도록 합니다.
husky
- *git hook(커밋, 푸쉬 등) 제어하는 npm 라이브러리
- git commit 시 eslint, prettier 실행 자동화하기
* git hook(🔗참고링크)
git과 관련한 어떤 이벤트가 발생했을 때 특정 스크립트를 실행할 수 있도록 하는 기능입니다. 크게 클라이언트 훅과 서버 훅으로 나뉘는데 클라이언트 훅은 commit, merge가 발생하거나 push가 발생하기 전 클라이언트에서 실행하는 훅입니다. 반면 서버 훅은 Git repository로 push가 발생했을 때 서버에서 실행하는 훅입니다.
lint-staged
- staged 된 파일만 특정 명령어 실행하는 도구
- commit 시 전체가 아니라 변경된 파일만 eslint, prettier 실행할 수 있도록 해준다.
5-1. 설치하기
husky 설치
/* npm */
npx husky-init && npm install
/* yarn 2+ */
yarn dlx husky-init --yarn2 && yarn
/* pnpm */
pnpm dlx husky-init && pnpm install
설치하면 pack.json 파일에 prepare 커멘드가 생깁니다.
또한 .husky/pre-commit 파일이 생성됩니다.
lint-staged 설치
/* npm */
npm i -D lint-staged
/* pnpm */
pnpm add -D lint-staged
5-2. package.json에 명령어 추가, pre-commit 파일에 명령어 추가
변경된 js, jsx, ts, tsx 파일만 밑 명령어를 실행할 수 있도록 해줍니다.
"scripts":{
...
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"dependencies": {
...
}
아까 생성된 pre-commit 파일에 다음 명령어를 추가해 줍니다.
/* npm */
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm test < - 이거 test 사용안하면 주석
npx lint-staged # 추가
/* pnpm */
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm test < - 이거 test 사용안하면 주석
pnpm dlx lint-staged # 추가
추가적으로 commitlint 를 통해서 커밋 컨벤션을 적용시킬 수도 있습니다. 이외에도 구글링을 통해 더 알아보도록 합시다.
6. storybook 세팅
🔗storybook 참고 링크(근데 좀 많이 옛날 버전입니다.)
6-1. storybook 설치
/* npm */
npx storybook@latest init
/* pnpm */
// pnpm dlx sb init storybook@latest init 사이트에서는 아래걸로 적혀있더라고요.
pnpm dlx storybook@latest init
or
// 위 명령어로 에러가 뜬다면 webpack5로 구성된 프로젝트에는 아래 명령어를 사용해 봅시다.
pnpm dlx sb init --builder webpack5
// 만약 dotenv-webpack 이 설치되어 있지 않다면 해당 패키지도 같이 설치해줍니다.
// 설치전에 dotenv가 기본적으로 장착된 cli인지 확인해봅시다.(Next.js 설치 X)
/* npm */
npm install dotenv-webpack --save-dev
/* pnpm */
pnpm add -D dotenv-webpack
❓prettier-plugin-tailwindcss랑 충돌이 일어나요ㅠㅠ
storybook이랑 prettier-plugin-tailwindcss랑 충돌이 일어나는 버그가 있습니다. tailwind prettier를 잠시 지워주고 storybook을 먼저 설치 후에 prettier-plugin-tailwindcss를 설치하면 됩니다.
❓Operationa not permitted 에러
vscode를 관리자 권한으로 시작후에 하면 됩니다.
storybook eslint 설치
🔗storybook official eslint 참고링크
/* npm */
npm install eslint-plugin-storybook --save-dev
/* pnpm */
pnpm add -D eslint-plugin-storybook
eslint 파일에도 추가해줍니다.
{
...
"extends": [..., "plugin:storybook/recommended", ...]
...
}
6-2. tsconfig.json 과 맞춰서 절대경로 세팅해주기
.storybook / main.ts 파일을 다음과 같이 일일이 세팅하는 방식도 있지만,
const path = require("path");
module.exports = {
webpackFinal: async (config) => {
// 상대 경로 기준은 main.ts 파일 경로입니다.
config.resolve.alias["@"] = path.resolve(__dirname, "../src");
config.resolve.alias["@/components"] = path.resolve(
__dirname,
"../src/components"
);
config.resolve.alias["@/hooks"] = path.resolve(__dirname, "../src/hooks");
config.resolve.alias["@/lib"] = path.resolve(__dirname, "../src/lib");
config.resolve.alias["@/pages"] = path.resolve(__dirname, "../src/pages");
config.resolve.alias["@/services"] = path.resolve(
__dirname,
"../src/services"
);
config.resolve.alias["@/styles"] = path.resolve(__dirname, "../src/styles");
config.resolve.alias["@/types"] = path.resolve(__dirname, "../src/types");
config.resolve.alias["@/utils"] = path.resolve(__dirname, "../src/utils");
return config;
},
.
.
.
}
tsconfig-paths-webpack-plugin 설치
머리가 아찔해져서 다른 방법을 찾아봤습니다. tsconfig-paths-webpack-plugin 라는 dev용 라이브러리가 있었습니다.
# npm
npm install -D tsconfig-paths-webpack-plugin
# pnpm
pnpm add -D tsconfig-paths-webpack-plugin
.storybook 폴더의 main.ts 파일을 다음과 같이 수정해줍니다. 그러면 stories.ts 폴더에서도 절대경로 사용이 가능해집니다.
import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
const config: StorybookConfig = {
stories: [ // 스토리 파일로 인식할 .stories. 파일을 어디서 찾을 것인지 패턴형태로 넣어줍니다.
'../stories/**/*.mdx',
'../**/*.stories.mdx',
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
{
name: '@storybook/addon-styling',
options: {},
},
],
framework: {
name: '@storybook/nextjs',
options: {},
},
docs: {
autodocs: 'tag',
},
webpackFinal: async config => {
config.resolve?.plugins?.push(new TsconfigPathsPlugin({}));
return config;
},
/* static asset (ex static 이미지) 경로 */
staticDirs: ['../public'],
};
export default config;
6-2. storybook 실행
실행 전에 tailwind 를 사용한다면 다음 addon을 설치해줍시다.(
/* npm */
npm i -D @storybook/addon-styling
/* pnpm */
pnpm add -D @storybook/addon-styling
설치 후에 자동으로 세팅할 수 있도록 해줍시다.
/* npm */
npm addon-styling-setup
/* pnpm */
pnpm addon-styling-setup
자동 세팅 후에는 대강 이런 모습이 되어있습니다.
main.ts
import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
const config: StorybookConfig = {
stories: [
'../stories/**/*.mdx',
'../**/*.stories.mdx',
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
{
name: '@storybook/addon-styling',
options: {},
},
],
framework: {
name: '@storybook/nextjs',
options: {},
},
docs: {
autodocs: 'tag',
},
webpackFinal: async config => {
config.resolve?.plugins?.push(new TsconfigPathsPlugin({}));
return config;
},
staticDirs: ['../public'],
};
export default config;
preview.ts
tailwind.css 파일의 위치를 import 해주세요!
import type { Preview } from '@storybook/react';
import { withThemeByClassName } from '@storybook/addon-styling';
import '../app/tailwind.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
/**
* @deescription next13.xx approuting 방식을 사용하면 appDirectory true를 줘야 한다는데 아직은 주석처리 해뒀음.
* * If your story imports components that use next/navigation, you need to set the parameter nextjs.appDirectory to true in your Story:
* 그렇다고 합니다. next/navigation 모듈을 사용(보통 useRouter를 꺼내서 사용하겠네요)하는 컴포넌트를 스토리로 import 할 때는 아래와 같이 스토리를 세팅합시다.
*
* @example
* ```ts
*
* // SomeComponentThatUsesTheRouter.stories.js
* import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation';
export default {
component: SomeComponentThatUsesTheNavigation,
};
export const Example = {
parameters: {
nextjs: {
appDirectory: true,
},
},
* ```
*
* * * 전부 다 app directory를 사용한다면 여기 preview.ts 에 설정해서 전체 default 로 먹여도 되겠습니다.
*/
},
decorators: [
// Adds theme switching support.
// NOTE: requires setting "darkMode" to "class" in your tailwind config
withThemeByClassName({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
}),
],
// * Context Provider로 감싸야 하는 경우 사용 (Global decorators)
// decorators: [
// (Story) => (
// <ThemeProvider theme="default">
// <Story />
// </ThemeProvider>
// ),
// ]
};
export default preview;
Button.tsx 파일 (예시임)
import cn from '@/utils/cn';
import { cva, VariantProps } from 'class-variance-authority';
import React, { ButtonHTMLAttributes, ComponentProps, HTMLAttributes, useMemo } from 'react';
import '../../app/tailwind.css';
export const ButtonVariants = cva(
/**
* @description 컴포넌트 @apply 도 잘 적용된다. cva 첫 번째 인자는 base: ClassValue 기본 깔고 가는 스타일이다. 없으면 그냥 "" 써주면 된다.
*/
`f-ic-jc active:scale-95 rounded-xl text-sm font-bold text-slate-100 transition-all shadow-md hover:scale-105 duration-200
`,
{
variants: {
/**
* 버튼 색깔
*/
intendedColor: {
/**
* @description 그냥 "shadow-sm bg-red-500" 이런 식으로 안에 적어도 되지만 좀 더 구분이 쉽게 하려면 [..., ...] 이런 식으로 적어도 된다.
*/
primary: ['shadow-lg', 'bg-red-700'],
secondary: ['shadow-none', 'bg-blue-700', 'ring ring-yellow-400'],
},
/**
* 버튼 크기
*/
size: {
default: '',
// md: 'w-[6.875rem] h-[2.375rem] text-[1rem] rounded-md',
md: 'px-14 h-[2.375rem] text-[1rem] rounded-md',
// lg: [' w-[21.875rem] h-[7.5rem] text-[3rem] rounded-2xl'],
lg: ' px-36 h-[7.5rem] text-[3rem] rounded-2xl',
wlg: 'w-[24rem] h-[5.25rem] text-[2rem]',
},
/**
* 대소문자
*/
textTransform: {
default: '',
capitalize: 'capitalize',
lowercase: 'lowercase',
uppercase: 'uppercase',
},
/**
* 에러용 버튼인지
*/
isError: {
true: true,
false: false,
},
/**
* @see https://manuarora.in/boxshadows
* box-shadow
*/
boxShadow: {
default: 'shadow-md',
Aesthetic: 'shadow-[0_3px_10px_rgb(0,0,0,0.2)]',
Euphonious: 'shadow-[0px_10px_1px_rgba(221,_221,_221,_1),_0_10px_20px_rgba(204,_204,_204,_1)]',
Jubilation: 'shadow-[rgba(0,_0,_0,_0.24)_0px_3px_8px]',
Mondegreen:
'shadow-[5px_5px_rgba(0,_98,_90,_0.4),_10px_10px_rgba(0,_98,_90,_0.3),_15px_15px_rgba(0,_98,_90,_0.2),_20px_20px_rgba(0,_98,_90,_0.1),_25px_25px_rgba(0,_98,_90,_0.05)]',
Nimble: 'shadow-[4.0px_8.0px_8.0px_rgba(0,0,0,0.38)]',
Ragnarok:
'shadow-[0_2.8px_2.2px_rgba(0,_0,_0,_0.034),_0_6.7px_5.3px_rgba(0,_0,_0,_0.048),_0_12.5px_10px_rgba(0,_0,_0,_0.06),_0_22.3px_17.9px_rgba(0,_0,_0,_0.072),_0_41.8px_33.4px_rgba(0,_0,_0,_0.086),_0_100px_80px_rgba(0,_0,_0,_0.12)]',
Stiglitz: 'shadow-[rgba(50,50,93,0.25)_0px_6px_12px_-2px,_rgba(0,0,0,0.3)_0px_3px_7px_-3px]',
},
},
/**
* @description 조건 충족하면 className 으로 가져온 추가 속성이 추가 적용됨.
*/
compoundVariants: [
{
size: 'md',
className: 'max-md:bg-primary',
},
{
size: 'lg',
className: 'max-lg:bg-secondary max-lg:px-10',
},
{
size: 'wlg',
className: 'uppercase ring ring-yellow-400',
},
{
isError: true,
className: 'text-red-500',
},
],
defaultVariants: {
intendedColor: 'primary',
size: 'default',
textTransform: 'default',
isError: false,
boxShadow: 'default',
},
},
);
// export type ButtonVariantProps = {
// label?: string | number;
// } & React.HTMLAttributes<HTMLButtonElement> &
// VariantProps<typeof ButtonVariants>;
export type ButtonVariantProps = {
/**
* 버튼 이름
*/
label?: string | number;
/**
* 버튼 이름 또는 svg 등
*/
children?: React.ReactNode;
/**
* 추가할 커스텀 스타일
*/
className?: string;
/**
* 에러가 발생했는지 여부
*/
isError?: boolean;
/**
* 마우스 클릭 시 콜백함수
*/
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/**
* 마우스 호버 시 콜백함수
*/
onMouseOver?: () => void;
/**
* 버튼 키보드로 눌릴 시 콜백함수
*/
onKeyDown?: () => void;
/**
* 버튼 type
*/
type?: ButtonHTMLAttributes<HTMLButtonElement>['type'];
// type: ComponentProps<'button'>['type'];
// type: Exclude<ButtonHTMLAttributes<HTMLButtonElement>['type'], 'reset'>;
} & VariantProps<typeof ButtonVariants>;
// & ComponentProps<'button'>;
// React.HTMLAttributes<HTMLButtonElement>;
/**
* @description 필수 속성 추가한 타입이다. intendedColor는 무조건 입력할 수 있게 했다.
*/
export interface IbuttonProps
extends Omit<ButtonVariantProps, 'intendedColor'>,
Required<Pick<ButtonVariantProps, 'intendedColor'>> {}
const Button = ({
children,
className,
type,
intendedColor,
size,
label,
textTransform,
isError,
boxShadow,
onClick,
onMouseOver,
onKeyDown,
}: IbuttonProps) => {
/**
* @description twMerge가 있어야 cva 첫 번째 인자 base classValue를 덮을 수 있음.
*/
// return <button className={ButtonVariants({intendedColor, size, className})} {...rest}>{label? label : children}</button>
return (
<button
className={cn(
ButtonVariants({
intendedColor,
size,
className,
textTransform,
isError,
boxShadow,
}),
)}
type={type !== 'submit' ? 'button' : 'submit'}
onClick={onClick}
onMouseOver={onMouseOver}
onKeyDown={onKeyDown}
>
{label || children}
</button>
);
};
export default Button;
Button.stories.ts 파일(예시)
스토리북이 버전업이 되고 7.3xx 버전이 되면서 이전 방법은 deprecated가 되었습니다.
타입을 모든 버튼마다 계속 주기 귀찮아서 ButtonMaker 함수를 만들고 bind를 해주었습니다.
import type { Meta, StoryObj } from '@storybook/react';
import Button from '@/components/button/Button';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: 'Practice/Button',
component: Button,
/**
* parameter 는 story의 정적인 메타데이터를 뜻한다. 모든 적용 순서는 global(preview.js) < component < story 순서이다. 오른쪽이 더 강력하여 최종적용된다.
*/
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
argTypes: {},
/**
* * 데코레이터는 기능적으로 추가적인 렌더링을 통해 감쌀 수 있는 방법
* 렌더링 된 부분을 추가적인 마크업을 작성하고 데코레이터를 이용해 감싸게 된다면 수정
* 마찬가지로 global(preview.js)가 있음.
*/
// decorators: [
// (Story) => (
// <div style={{ margin: '3em' }}>
// <Story/>
// </div>
// ),
// ],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
const ButtonMaker = (args: Partial<Story>) => ({ ...args });
// const BaseButton = ButtonMaker.bind({});
/**
* * 방법 1
*/
export const TestPrimary = ButtonMaker({
args: {
intendedColor: 'primary',
label: 'test',
size: 'md',
},
});
/**
* * 방법 2
*/
export const TestSecondary = ButtonMaker({});
TestSecondary.args = {
intendedColor: 'secondary',
size: 'lg',
label: 'bind button',
textTransform: 'capitalize',
};
/**
* * 방법 3
*/
export const Primary: Story = {
args: {
intendedColor: 'primary',
label: 'Primary',
size: 'default',
onClick: () => console.log('test'),
},
};
export const Secondary: Story = {
args: {
intendedColor: 'secondary',
label: 'Secondary',
size: 'default',
},
};
export const Large: Story = {
args: {
intendedColor: 'primary',
label: 'Large',
size: 'lg',
},
};
export const Small: Story = {
args: {
intendedColor: 'secondary',
label: 'Small',
size: 'default',
},
};
잘 적용이 됩니다.


7. tailwind 세팅
tailwind를 설치합시다(안했다면!)
/* npm */
npm install -D tailwindcss postcss autoprefixer
/* pnpm */
pnpm add -D tailwindcss postcss autoprefixer
/* npm */
npx tailwind init -p
혹은
npx tailwind init ./tailwind.config.js
/* pnpm */
pnpm exec tailwind init -p
혹은
pnpm exec tailwind init ./tailwind.config.js
7-1. tailwind 관련 설치(prettier, settings 등)를 다룹니다.(선택)
tailwind 관련 prettier도 설치
tailwind를 쓴다면(안 쓰면 선택사항이라는 뜻) tailwind 관련 prettier도 설치해 줍시다. (매우 강력히 권장)
/* npm */
npm i -D prettier-plugin-tailwindcss
/* pnpm */
pnpm add -D prettier-plugin-tailwindcss
clsx, class-variance-authority, tailwind-merge, @tailwindcss/container-queries 설치
마찬가지로 tailwind를 쓴다면 강력히 설치를 권장드립니다.
/* npm */
npm i class-variance-authority clsx tailwind-merge @tailwindcss/container-queries
/* pnpm */
pnpm add class-variance-authority clsx tailwind-merge @tailwindcss/container-queries
tailwind prettier, intellisense를 위한 정규표현식 세팅(.vscode)
workspace setting / user setting 중 선택하셔서 세팅하시면 됩니다.
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["(?:twMerge|twJoin)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"],
":\\s*?[\"'`]([^\"'`]*).*?,"
],
"tailwindCSS.classAttributes": [
"class",
"className",
"ngClass",
".*Styles.*",
]
설치하는 이유와 관련해서 아주 짧게 글을 적었습니다. 더 자세히 보고 싶으시면 아래 링크를 보시면 됩니다.
[Tailwind] Tailwind CSS with CVA in react
0. tailwind css 설치 1. tailwind-merge, clsx 라이브러리 설치 npm i --save tailwind-merge tailwind-merge 라이브러리에서 twMerge와 twJoin이 있는데 twMerge를 사용할 것입니다. twMerge 함수를 사용하게 되면 예를 들어 다
olimjo.tistory.com
7-2. 최종 workSpace settings 예시(.vscode/settings.json 파일)
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// VSCode에 내장되어 있는 자바스크립트 포맷팅 기능을 사용하지 않고 Prettier 익스텐션을 사용하기 위해서 설정.
"[javascript]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// ESLint에 의한 자동 수정 기능을 활성화하기 위해서 설정
"editor.codeActionsOnSave": {
// For ESlint
"source.fixAll.eslint": true
},
// 에디터 바깥으로 포커스가 이동 시 파일을 자동으로 저장하기 위해서 설정
"files.autoSave": "onFocusChange",
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["(?:twMerge|twJoin)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"],
":\\s*?[\"'`]([^\"'`]*).*?,"
],
"tailwindCSS.classAttributes": ["class", "className", "ngClass", ".*Styles.*"],
// "tailwindCSS.rootFontSize": 10,
/* tailwind unknown error resolve */
"css.lint.unknownAtRules": "ignore"
}
7-3. .prettierrc.cjs 파일 생성 및 세팅
module.exports = {
// doubleQuote: true,
// 문자열은 singleQuote로 ("" -> '')
singleQuote: true,
jsxSingleQuote: true,
//코드 마지막에 세미콜론이 있게 formatting
semi: true,
// 들여쓰기 너비는 2칸
tabWidth: 2,
// 배열 키:값 뒤에 항상 콤마를 붙히도록 formatting
trailingComma: 'all',
// 코드 한줄이 maximum 80칸
printWidth: 80,
// 화살표 함수가 하나의 매개변수를 받을 때 괄호를 생략하게 formatting
arrowParens: 'avoid',
// windows에 뜨는 'Delete cr' 에러 해결
endOfLine: 'auto',
bracketSpacing: true, // 중괄호 내에 공백 사용,
// 주석에서 '//' 다음에 예외 블록, 공백 또는 탭이 오도록 설정
// Expected exception block, space or tab after '//' in comment.eslint
// 에러를 해결하기 위한 설정입니다.
// 원하는 대로 예외 블록, 공백 또는 탭을 지정해 주세요.
// 예를 들어, 'always'로 설정하면 '//' 다음에 공백이 필요합니다.
// 'never'로 설정하면 '//' 다음에 공백이 오면 에러가 발생합니다.
// commentLineExceptions: ['always'],
// annotationSpacing: 'always',
/* 아래는 tailwind 관련 세팅 */
// By default this plugin only sorts classes in the class attribute as well as any framework-specific equivalents like class, className, :class, [ngClass], etc.
// You can sort additional attributes using the tailwindAttributes option, which takes an array of attribute names:
tailwindAttributes: ['Styles'],
// prettier.config.js in clsx cva
// prettier.config.js in template literals
tailwindFunctions: ['clsx', 'tw'],
// prettier-plugin-tailwindcss must be set on the last in plugin Array.
// One limitation with this approach is that prettier-plugin-tailwindcss must be loaded last, meaning Prettier auto-loading needs to be disabled. You can do this by setting the pluginSearchDirs option to false and then listing each of your Prettier plugins in the plugins array:
plugins: ['prettier-plugin-tailwindcss'],
// pluginSearchDirs: false, // Prettier v2.x only
};
7-4. container queries 추가(선택)
tailwind css에서 container queries를 자체적으로 제공하지는 않고(container-queries와 다르지만 기존에도 container라는 게 있긴 합니다.) tailwind를 만든 팀인 Tailwind Labs에서 공식적으로 tailwindcss/container-queries라는 라이브러리로 추가 제공하고 있습니다.
/* npm */
npm i @tailwindcss/container-queries
/* pnpm */
pnpm add @tailwindcss/container-queries
그 다음 tailwind.config.ts 파일에서 import후에 plugins 에 넣어줍니다.
import Container from '@tailwindcss/container-queries';
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import addComponentsStyles from './style/twComponents';
import addUtilityStyles from './style/twUtility';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
// Toggle dark-mode based on data-mode="dark"
darkMode: ['class', '[data-mode="dark"]'],
theme: {
extend: {
colors: {
primary: '#BA55D3',
},
keyframes: {
emerge: {
'0%, 99%': { right: '0', opacity: '0.5' },
'20%': { right: '5%', opacity: '1' },
'58%': { right: '5%', opacity: '1' },
'77%': { right: '3%', opacity: '1' },
'85%': { right: '7%', opacity: '1' },
'100%': { display: 'none' },
},
shake: {
'0%': { transform: 'translateX(0)' },
'20%': { transform: 'translateX(-10%)' },
'40%': { transform: 'translateX(10%)' },
'60%': { transform: 'translateX(-10%)' },
'80%': { transform: 'translateX(10%)' },
'100%': { transform: 'translateX(0)' },
},
'tw-pop': {
from: { transfrom: 'translateY(200px)' },
to: { transfrom: 'translateY(0px)' },
},
},
// animation: name duration timing-function delay iteration-count direction fill-mode play-state
animation: {
emerge: 'emerge 3s ease-in-out forwards',
shake: 'shake 2s',
},
},
},
// devtools setting
corePlugins: {
textOpacity: false,
backgroundOpacity: false,
borderOpacity: false,
divideOpacity: false,
placeholderOpacity: false,
ringOpacity: false,
},
plugins: [
plugin(({ addComponents, addUtilities, matchUtilities, theme }) => {
addComponents(addComponentsStyles);
addUtilities(addUtilityStyles);
matchUtilities(
{
tab: value => ({
tabSize: value,
}),
},
{ values: theme('tabSize') },
);
}),
Container,
],
};
export default config;
7-5 🚀 css 파일에서 container queries 관련 문법을 썼을 때 찾지 못하는(unknown) 경우가 있습니다.
파일의 문제가 아니라 VS code 에서 아직 해당 문법을 공식적으로 지원하지 않는 것입니다. MDN 문서를 링크하여 container queries 문법을 지원하도록 명시해줍니다.
workspace setting 하기를 통해 .vscode 폴더 밑에
custom-css.json (이름 상관없음) 이라는 파일을 만들어줍니다.(🔗참고 링크)
{
"version": 1.1,
"$schema": "https://raw.githubusercontent.com/microsoft/vscode-css-languageservice/master/docs/customData.schema.json",
"atDirectives": [
{
"name": "@container",
"description": {
"kind": "markdown",
"value": "The **@container** CSS at-rule is a conditional group rule that applies styles to a containment context."
},
"references": [
{
"name": "MDN reference",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/@container"
}
],
"browsers": ["E105", "FF110", "C105", "S16.0", "O91"]
}
],
"properties": [
{
"name": "container-type",
"description": {
"kind": "markdown",
"value": "The **container-type** CSS property is used to define the type of containment used in a container query."
},
"references": [
{
"name": "MDN reference",
"url": "https://developer.mozilla.org/docs/Web/CSS/container-type"
}
],
"browsers": ["E105", "FF110", "C105", "S16.0", "O91"]
},
{
"name": "container-name",
"description": {
"kind": "markdown",
"value": "The **container-name** CSS property specifies a list of query container names used by the **@container** at rule in a container query."
},
"references": [
{
"name": "MDN reference",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/container-name"
}
],
"browsers": ["E105", "FF110", "C105", "S16.0", "O91"]
},
{
"name": "container",
"description": {
"kind": "markdown",
"value": "The **container** shorthand CSS property establishes the element as a query container and specifies the name or name for the containment used in a container query."
},
"references": [
{
"name": "MDN reference",
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS/container"
}
],
"browsers": ["E105", "FF110", "C105", "S16.0", "O91"]
}
]
}
./vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// VSCode에 내장되어 있는 자바스크립트 포맷팅 기능을 사용하지 않고 Prettier 익스텐션을 사용하기 위해서 설정.
"[javascript]": {
"editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// ESLint에 의한 자동 수정 기능을 활성화하기 위해서 설정
"editor.codeActionsOnSave": {
// For ESlint
"source.fixAll.eslint": true
},
// 에디터 바깥으로 포커스가 이동 시 파일을 자동으로 저장하기 위해서 설정
"files.autoSave": "onFocusChange",
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["(?:twMerge|twJoin)\\(([^\\);]*)[\\);]", "[`'\"`]([^'\"`,;]*)[`'\"`]"],
":\\s*?[\"'`]([^\"'`]*).*?,"
],
"tailwindCSS.classAttributes": ["class", "className", "ngClass", ".*Styles.*"],
// "tailwindCSS.rootFontSize": 10,
/* tailwind unknown error resolve */
"css.lint.unknownAtRules": "ignore",
// 아래 링크 참조해서 container queries unknown 에러 껐음.
// https://github.com/wileycoyote78/custom-css/tree/main
"css.customData": ["./.vscode/custom-css.json"]
}
맨 밑의 라인을 추가해줍시다.
7-6. postcss-import, tailwindcss/nesting (선택)
tailwindcss/nesting
tailwindcss/nesting 은 따로 설치하는 것은 아니고 nesting 문법을 사용할 때 postcss plugin에 명시해야 하는 것입니다.
(🔗링크 참고 . 안 해도 에러는 안 뜨고 경고만 계속 해줍니다.)
세팅 설명은 밑의 postcss-import 에서 postcss.config.js 파일과 같이 해주시면 됩니다.(postcss-import 는 필요없으면 빼도 됨.)
postcss-import
tailwind.css 파일이 너무 길어져서 다른 css 파일로 분리해서 사용하고 싶으면 tailwindcss 문법을 다른 css 파일로 분리시켜 import 해서 사용할 수 있도록 해줍시다.
postcss-import 를 설치해줍니다.
/* npm */
npm i -D postcss-import
/* pnpm */
pnpm add -D postcss-import
사용방법
CustomComponent.module.css
@layer components {
.last-test {
@apply border border-red-500 text-lg font-bold text-red-600;
}
}
tailwind.css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import './style/CustomComponent.module.css';
@import 'tailwindcss/utilities';
/* @tailwind base;
@tailwind components;
@tailwind utilities; */
...
base에 추가하는 파일은 base 밑에,
components에 추가하는 파일은 components 밑에,
utilities에 추가하는 파일은 utilities 밑에 각각 순서를 맞춰서 import 해줍니다.
import 규칙을 지키지 않으면 인식하지 않습니다.
tailwindintellisense가 전부 잘 인식해줍니다.
postcss.config.js
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
};
7-7. 자바스크립트 파일로 css 분리
자바스크립트 파일로 css 분리
css 파일로 분리하는 것이 싫다면, js/ts 파일로도 custom utility class를 추가할 수 있습니다. 서버와 클라이언트 구별없이 잘 적용됩니다. 또, 자바스크립트 파일로 분리했을 때만의 장점이 있는데 밑에서 설명하도록 하겠습니다.
twUtility.ts 파일(이름 상관 없음. 확장자 js로 해도 됨)
import { CSSRuleObject } from 'tailwindcss/types/config';
const addUtilityStyles: CSSRuleObject | CSSRuleObject[] = {
'.pause': {
animationPlayState: 'paused',
MozAnimationPlayState: 'paused',
WebkitAnimationPlayState: 'paused',
OAnimationPlayState: 'paused',
},
'.running': {
animationPlayState: 'running',
MozAnimationPlayState: 'running',
WebkitAnimationPlayState: 'running',
OAnimationPlayState: 'running',
},
'.f': {
'@apply flex': {},
},
'.f-ic-jc': {
'@apply flex items-center justify-center': {},
},
'.f-fc': {
'@apply flex flex-col': {},
},
'.f-fc-jc': {
'@apply f-fc justify-center': {},
},
'.f-fc-ic': {
'@apply f-fc items-center': {},
},
'.f-fc-ic-jc': {
'@apply f-fc items-center justify-center': {},
},
'.f-fr': {
'@apply flex flex-row': {},
},
'.f-fr-ic': {
'@apply f-fr items-center': {},
},
'.f-jc': {
'@apply f justify-center': {},
},
'.f-fr-jc': {
'@apply f-fr justify-center': {},
},
'.f-fr-ic-jc': {
'@apply f-fr items-center justify-center': {},
},
'.f-fr-ic-jb': {
'@apply f-fr-ic justify-between': {},
},
'.f-fr-ic-js': {
'@apply f-fr-ic justify-start scale-110': {},
},
};
export default addUtilityStyles;
tailwind.config.ts
import 후에 plugin 내부에서 다음과 같이 사용해줍니다.
import Container from '@tailwindcss/container-queries';
import type { Config } from 'tailwindcss';
import defaultColors from 'tailwindcss/colors';
import plugin from 'tailwindcss/plugin';
import addComponentsStyles from './style/twComponents';
import addUtilityStyles from './style/twUtility';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
// Toggle dark-mode based on data-mode="dark"
darkMode: ['class', '[data-mode="dark"]'],
theme: {
extend: {
// backgroundImage: {
// 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
// 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
// },
colors: {
primary: '#BA55D3',
secondary: defaultColors.blue[300],
},
keyframes: {
emerge: {
'0%, 99%': { right: '0', opacity: '0.5' },
'20%': { right: '5%', opacity: '1' },
'58%': { right: '5%', opacity: '1' },
'77%': { right: '3%', opacity: '1' },
'85%': { right: '7%', opacity: '1' },
'100%': { display: 'none' },
// '100%': { visibility: 'hidden' },
},
shake: {
'0%': { transform: 'translateX(0)' },
'20%': { transform: 'translateX(-10%)' },
'40%': { transform: 'translateX(10%)' },
'60%': { transform: 'translateX(-10%)' },
'80%': { transform: 'translateX(10%)' },
'100%': { transform: 'translateX(0)' },
},
'tw-pop': {
from: { transfrom: 'translateY(200px)' },
to: { transfrom: 'translateY(0px)' },
},
},
// animation: name duration timing-function delay iteration-count direction fill-mode play-state
animation: {
emerge: 'emerge 3s ease-in-out forwards',
shake: 'shake 2s',
},
},
},
corePlugins: {
textOpacity: false,
backgroundOpacity: false,
borderOpacity: false,
divideOpacity: false, // bg-red-500/10 이런 거 devtools 에서 막는다고함?
placeholderOpacity: false,
ringOpacity: false,
},
plugins: [
plugin(({ addComponents, addUtilities, matchUtilities, theme }) => {
addComponents(addComponentsStyles);
addUtilities(addUtilityStyles);
matchUtilities(
{
tab: value => ({
tabSize: value,
}),
},
{ values: theme('tabSize') },
);
}),
Container,
],
};
export default config;
자바스크립트 파일로 분리했을 때의 장점은 컴포넌트 내부에서도 intellisense extension이 해당 커스텀 유틸리티 클래스에 대한 자동 완성을 지원한다는 점입니다.
컴포넌트 내부/ js/ts 파일 내부에서도 tailwindcss intellisense 가 잘 지원해주는 모습을 볼 수 있습니다.
이와 관련해서 국내 자료는 아예 없었습니다. custom utility class 자동완성을 위한 방법에 대한 해결법을 tailwindcss github issue에서의 대화를 통해 찾아볼 수 있었습니다.



.vscode/settings.json 정규표현식을 잘 따라와주셨다면 intellisense extension이 문제없이 잘 적용될 것입니다.
tailwindcss-extend (2023.09.07 기준 2달(?) 전 즈음에 만들어짐.)
위의 방법을 쓰면 아직 tailwindcss에서 제공하지 않는 일부 css 프로퍼티를 jsx inline-style 처럼 camelCase로 css를 작성해야 하는 경우도 있었기에 그런 점에서 불편함을 느꼈습니다. 그래서 일반 css 파일의 내용을 그냥 불러와서 자동으로 변환해주는 파일이 있으면 좋겠다는 생각을 하여 찾아봤습니다.
그러다가 tailwindcss-extend 라는 dev용 라이브러리를 발견했습니다.(tailwindlabs에서 제공하는 공식 라이브러리는 아닙니다.)

위의 방법과 관련해서 자동으로 commonjs 파일을 생성하고, 위에서 언급했던 tailwindcss/plugin에서 제공하는 utility function을 통해서 css를 js가 읽을 수 있게 변환한 그 파일의 내용을 불러오도록 하는 것을 자동으로 해주는 npm 패키지를 tailwindcss-intellisense의 issue 대화목록에서 찾을 수 있었습니다.
/* npm */
npm i -D tailwindcss-extend
/* pnpm */
pnpm add -D tailwindcss-extend
아래와 같이 명령어를 입력하면 -p가 패턴을 통해서 css 파일을 찾아줍니다. 여러 flag가 있으니 직접 참고하시면 될 것 같습니다.
/* npm */
npm "tailwindcss-extend -p \"app/**/*.css\" --watch"
/* pnpm */
pnpm "tailwindcss-extend -p \"app/**/*.css\" --watch",
// 패턴 형태로 모든 파일을 훑을 수 있으나 모든 파일을 훑는 동안 tailwindcss-extend.cjs 파일 내용이 계속 달라집니다.
// 따라서 몇 초 주기로 깜빡임 현상이 있습니다.
// 그래서 정확한 tailwind.css 파일의 위치를 적는 것을 추천합니다. 그러면 깜빡임 현상이 일어나지 않습니다.
// [예시]
pnpm "tailwindcss-extend -p \"./src/tailwind.css\" --watch",
shell script 설정(명령어 병렬 실행을 위해 npm-run-all 을 사용하였습니다.)
"scripts": {
"dev": "run-p dev:*",
"dev:tailwindcss": "tailwindcss-extend -p \"./**/tailwind.css\" --watch",
"dev:vite": "vite",
...
}
"devDependencies" ...
...
아래와 같이 .cjs 파일확장자에서 자동으로 tailwindcss 공식 plugin 유틸리티 함수를 사용하는 것을 확인할 수 있습니다.
/* tailwindcss-extend.cjs */
const handler = (api) => {
api.addBase({"h1":{"@apply text-2xl":true},"h2":{"@apply text-xl":true},"h3":{"@apply text-lg":true},"input,\n textarea":{"@apply outline-none":true},"textarea":{"@apply resize-none":true},":root":{"--default-content":"''","--flexible":"80vw","--only-number":"80","--custom-number":"50","--primary-color":"#ba55d3"},".card":{"--number-variable":"100"}});
api.addComponents({".ul-underline":{"@apply relative cursor-pointer after:absolute after:bottom-0 after:left-1/2 after:h-1 after:w-0 after:-translate-x-1/2 after:rounded-sm after:bg-red-600 after:transition-all after:duration-500 hover:after:w-11/12":true},".tw-underline":{"cursor":"pointer","position":"relative","&::after":{"position":"absolute","content":"var(--default-content)","bottom":"0","left":"50%","borderRadius":"0.125rem","width":"0","height":"0.25rem","backgroundColor":"#dc2626","transitionProperty":"all","transitionTimingFunction":"cubic-bezier(0.4, 0, 0.2, 1)","transitionDuration":"500ms","transform":"translateX(-50%)"},"&:hover::after":{"width":"91.666667%"}},".grid-area":{"display":"grid","gridTemplateColumns":"repeat(5, minmax(100px, 1fr))","gridAutoRows":"minmax(100px, 1fr)","width":"100%","backgroundColor":"var(--primary-color)","@media not screen and (min-width: 768px)":{"backgroundColor":"aqua"}},".grid-min-content":{"display":"grid","gridTemplateColumns":"1fr minmax(max-content, 1fr) 1fr","gridTemplateRows":"150px"},".grid-auto-content":{"display":"grid","gridTemplateRows":"50px","gridAutoFlow":"column"},".roof":{"@apply flex items-center justify-center text-lg font-bold":true},".check-flexible":{"width":"var(--flexible)"},".check-number":{"width":"calc(var(--only-number) * 1vw)"},".parent-container":{"containerName":"main-container","containerType":"inline-size","backgroundColor":"#dc2626"},"@container main-container (max-width: 200px)":{".parent-container > span":{"fontSize":"0.75em + 2cqi","textDecoration":"underline","textTransform":"capitalize","color":"#ba55d3"},".parent-container":{"backgroundColor":"#ba55d3"}},".super":{"@media screen and (max-width: 768px)":{"backgroundColor":"green"}},".test":{"@screen md":{"@apply bg-yellow-300":true}}});
api.addUtilities({".f":{"@apply flex":true},".f-ic-jc":{"@apply flex items-center justify-center":true},".f-fc":{"@apply flex flex-col":true},".f-fc-jc":{"@apply f-fc justify-center":true},".f-fc-ic":{"@apply f-fc items-center":true},".f-fc-ic-jc":{"@apply f-fc items-center justify-center":true},".f-fr":{"@apply flex flex-row":true},".f-fr-ic":{"@apply f-fr items-center":true},".f-jc":{"@apply f justify-center":true},".f-fr-jc":{"@apply f-fr justify-center":true},".f-fr-ic-jc":{"@apply f-fr items-center justify-center":true},".f-fr-ic-jb":{"@apply f-fr-ic justify-between":true},".f-fr-ic-js":{"@apply f-fr-ic scale-110 justify-start":true},".custom-flex":{"@apply f-ic-jc text-lg text-primary/70":true},".happy-custom":{"@apply rounded-full border border-primary px-3":true},".normal-button":{"@apply border border-solid border-primary text-lg font-bold":true},".pause":{"animationPlayState":"paused","WebkitAnimationPlayState":"paused","MozAnimationPlayState":"paused","OAnimationPlayState":"paused"},".running":{"animationPlayState":"running","WebkitAnimationPlayState":"running","MozAnimationPlayState":"running","OAnimationPlayState":"running"},".custom-last":{"@apply top-0":true}});
};
const config = {};
module.exports = {
handler,
config,
};
이후 이 모듈을 가져오면 끝입니다.
tailwind.config.ts 파일의 plugins 배열 안에서 모듈을 불러와줍니다.
import Container from '@tailwindcss/container-queries';
import type { Config } from 'tailwindcss';
import defaultColors from 'tailwindcss/colors';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
......
plugins: [
require('./tailwindcss-extend.cjs'),
Container,
],
};
export default config;
추가적으로 tailwind.config.ts 파일에서 플러그인으로 사용하고 있어서 css가 중복돼서 빌드 용량이 늘어나는지 검증을 해봤으나 전후 빌드 크기의 차이가 없었습니다. tailwind 자체적으로 tree shaking을 해주는 것 같습니다.


built in 시간도 매번 다르게 나와서 동일하다고 판단했습니다.