HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🌟
Programmers 최종 플젝 6조
/
👨🏻‍💻
[프론트] 기초 환경 세팅
👨🏻‍💻

[프론트] 기초 환경 세팅

💡
에브리벤트의 구체적인 프론트엔드 기초 환경 세팅 과정을 공유하고자 이 글을 씁니다. 모든 세팅의 원천은 여기로 하면 좋을 것 같아요! -Jason
 

목차

목차기술 스택(211130~) /React, Next.js, TypeScriptEmotionPages/home/index.tsxESLint.eslintrcPrettier.eslintrc.eslintrcStorybookStorybook 작동 테스트Storybook Test → Button.stories.tsxstorybook 작동 테스트Button.stories.tsx.storybook/main.js절대 경로 설정절대경로를 위한 src 생성 및 디렉토리 구조 변경추가 - @ 절대 경로 설정src/stories/Button.stories.tsxStyleLint기본 설치세부 패키지 설명PostCSS Syntax Plugin 설치@stylelint/postcss-css-in-js.stylelintrc.js.stylelintignore 적용.stylelintignorepackage.json 린트 검사 구문 설정.vscode참고 자료

기술 스택(211130~) /

저희 팀은 현재 다음과 같은 기술 스택을 사용하고 있습니다.
  • React
  • Next.js
  • Emotion
  • Storybook
  • TypeScript
  • eslint
  • prettier
  • StyleLint
💡
구체적인 버전에 따라, 실제 세팅에서 오류가 날 수 있음을 주의하여, 이 글을 통해 서로가 안전하게 세팅을 관리할 수 있도록 글을 씁니다.
 

React, Next.js, TypeScript

next.js는 CRA처럼 간단한 세팅을 제공합니다. 따라서 typescript 옵션까지 넣어주어 간단하게 설치를 하였습니다.
yarn create next-app --typescript

Emotion

이모션의 경우, css prop을 사용할 때에 있어 번거로운 점이 발생합니다.
바로 Pragma를 사용해야 하는 것인데요. jsx문법을 파싱할 때 css를 해석할 수 없기 때문입니다. 참고
pragma란 다음과 같은 주석과 같이 표기함으로써 babel을 대체하는 플러그인입니다.
/** @jsx jsx */ import { jsx } from '@emotion/react'
꽤나 번거로운데요. 따라서 이를 해결하기 위해 우리는 다음과 같이 직접 babel세팅을 커스터마이징 하였습니다.
{ "presets": [ [ "next/babel", { "preset-react": { "runtime": "automatic", "importSource": "@emotion/react" } } ] ], "plugins": ["@emotion/babel-plugin"] }
또한, tsconfig.json 역시 컴파일 당시에는 css에 대한 정보를 알지 못합니다.
따라서 해당 이슈에 관한 추천이 높은 글을 토대로 다음을 tsconfig.json에 추가했습니다.
"jsx": "react-jsx", "jsxImportSource": "@emotion/react",
이제, TS와 next에 탑재된 babel과의 충돌을 피하면서 Emotion의 css Prop을 사용할 수 있었습니다.
 
예시를 적용하면서, 성공적으로 작동함을 재확인하였습니다.

Pages/home/index.tsx

import styled from '@emotion/styled'; import React from 'react' const Message = styled.div` color: red; `; function index() { const message: string = 'HI'; return ( <Message css={{background: 'blue'}}> {message} </Message> ) } export default index
notion image
 

ESLint

ESLint와 같은 경우 CLI가 잘 되어 있으므로 다음과 같이 세팅하였습니다.
yarn eslint --init
💡
√ How would you like to use ESLint? · style √ What type of modules does your project use? · esm √ Which framework does your project use? · react √ Does your project use TypeScript? · Yes √ Where does your code run? · browser √ How would you like to define a style for your project? · guide √ Which style guide do you want to follow? · airbnb √ What format do you want your config file to be in? · JavaScript
만들어졌지만 다음과 같은 오류가 발생합니다.
Restrict file extensions that may contain JSX (react/jsx-filename-extension)
기본적으로 JSX가 아니면 airbnb에서는 허락하지 않습니다.
따라서 우리는 다음 룰을 추가해주며, 확장자가 tsx임을 알려주었습니다.

.eslintrc

rules: { "react/jsx-filename-extension": [1, { extensions: [".ts", ".tsx"] }], "react/require-default-props": "off" },
 

Prettier

여기까지 진행했다면, 모든 모듈에서 에러가 나올텐데요. 현재 singleQuote를 쓰라고 할 것입니다. 그러나 이를 저장할 경우, 현재 prettier의 포맷팅과 충돌하여 수정이 되지 않습니다.
 
따라서 우리는 포맷터인 Prettier도 ESLint와 호환이 가능하도록 설정해주었습니다.
yarn add -D eslint-config-prettier eslint-plugin-prettier prettier
 
eslint-config-prettier은 eslint와 어긋나는 프리티어 규칙이 충돌할 수 있는 설정을 꺼버립니다.
eslint-config-prettier의 권장사항에 따라 다음과 같이 코드를 추가합니다.

.eslintrc

extends: ["plugin:react/recommended", "airbnb", "prettier"],
 
그리고 eslint-plugin-prettier을 설정해준다. 이 역시 공식 문서의 추천 사항에 따라, Recommended Configuration으로 간단히 설정하였습니다.

.eslintrc

extends: [ 'plugin:react/recommended', 'airbnb', 'prettier', 'plugin:prettier/recommended', ],
참고로 Recommended Configuration(plugin:prettier/recommended)은 다음과 같은 기능을 간단히 플러그인화하여 처리합니다.
{ "extends": ["prettier"], "plugins": ["prettier"], "rules": { "prettier/prettier": "error", "arrow-body-style": "off", "prefer-arrow-callback": "off" } }
 

Storybook

컴포넌트를 테스트하기에 적합한 툴로, 저희는 스토리북을 선택했습니다. 이를 통해 컴포넌트를 간단히 구현하면, 테스트할 수 있습니다.
기존 공식문서를 참고하여 설치합니다.
npx sb init
스토리북은 현재의 환경을 인식하여 자연스럽게 호환 가능하도록 설치할 수 있습니다.
 

Storybook 작동 테스트

스토리북은 정확히 말하자면, 개발 환경과 같은 서버를 공유하지 않습니다. 따라서 현재 개발 서버에서 정상적으로 작동한다 하더라도, 스토리북에서는 오류가 날 수 있는데요.
따라서 다음과 같이 Button 컴포넌트를 만들어 테스트를 해보았습니다.
import { css } from '@emotion/react'; import styled from '@emotion/styled'; import React from 'react'; interface StyledButtonProps { width: number; height: number; } interface ButtonProps extends StyledButtonProps { children: string; } const StyledButton = styled.button` ${({ width, height }: ButtonProps) => width && height && css` width: ${width}px; height: ${height}px; background: orange; `} `; const Button = ({ width, height, children }: ButtonProps) => { return ( <StyledButton width={width} height={height}> {children} </StyledButton> ); }; export default Button;
import styled from '@emotion/styled'; import Button from '../../components/Button'; const Message = styled.div` color: red; `; function index() { const message: string = 'HI'; return ( <> <Message css={{ background: 'blue' }}>{message}</Message> <Button width={96} height={32}> Click! </Button> </> ); } export default index;
notion image
 
그렇다면, 스토리북을 사용하기 위해 스토리북에서는 어떨지 확인을 해보아야 했습니다.

Storybook Test → Button.stories.tsx

import React from 'react'; import Button, { ButtonProps } from '../components/Button'; export default { title: 'component/Button', component: Button, argTypes: { width: { defaultValue: 96, control: { type: 'range', min: 8, max: 500, step: 2 }, }, height: { defaultValue: 32, control: { type: 'range', min: 8, max: 500, step: 2 }, }, children: { type: 'text', defaultValue: 'Click!', }, }, }; const Template = (args: ButtonProps) => <Button {...args} />; export const Default = Template.bind({});
 

storybook 작동 테스트

먼저 간단한 Button 컴포넌트를 만들어보자.
import { css } from '@emotion/react'; import styled from '@emotion/styled'; import React from 'react'; interface StyledButtonProps { width: number; height: number; } interface ButtonProps extends StyledButtonProps { children: string; } const StyledButton = styled.button` ${({ width, height }: ButtonProps) => width && height && css` width: ${width}px; height: ${height}px; background: orange; `} `; const Button = ({ width, height, children }: ButtonProps) => { return ( <StyledButton width={width} height={height}> {children} </StyledButton> ); }; export default Button;
이후 실제로 작동하는지 테스트한다.
import styled from '@emotion/styled'; import Button from '../../components/Button'; const Message = styled.div` color: red; `; function index() { const message: string = 'HI'; return ( <> <Message css={{ background: 'blue' }}>{message}</Message> <Button width={96} height={32}> Click! </Button> </> ); } export default index;
notion image
정상 작동을 확인했습니다!
따라서 스토리북에서도 적용이 가능한지 테스트하였습니다.

Button.stories.tsx

import React from 'react'; import Button, { ButtonProps } from '../components/Button'; export default { title: 'component/Button', component: Button, argTypes: { width: { defaultValue: 96, control: { type: 'range', min: 8, max: 500, step: 2 }, }, height: { defaultValue: 32, control: { type: 'range', min: 8, max: 500, step: 2 }, }, children: { type: 'text', defaultValue: 'Click!', }, }, }; const Template = (args: ButtonProps) => <Button {...args} />; export const Default = Template.bind({});
notion image
 
오류가 발생합니다. Emotion@11의 경로를 인식하지 못하는 문제가 발생했기 때문입니다. 이슈

.storybook/main.js

const path = require('path'); const toPath = (_path) => path.join(process.cwd(), _path); module.exports = { stories: [ '../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)', ], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], webpackFinal: async (config) => { return { ...config, resolve: { ...config.resolve, alias: { ...config.resolve.alias, '@emotion/core': toPath('node_modules/@emotion/react'), '@emotion/styled': toPath('node_modules/@emotion/styled'), 'emotion-theming': toPath('node_modules/@emotion/react'), }, }, }; }, };
notion image
잘 작동함을 확인했습니다.
 

절대 경로 설정

그렇다면 절대 경로를 설정하도록 하겠습니다. 절대 경로를 설정하면, 디렉토리 구조의 복잡성에 상관 없이 어떤 모듈인지 정확히 파악할 수 있다는 장점이 있습니다.
다행히도, next.js는 버전 9부터 src에 대한 디렉토리 절대경로를 지원한다고 합니다. [출처 - Next.js 공식문서]
 
따라서 다음과 같이 공식문서에 맞춰 디렉토리를 바꿔줬습니다.

절대경로를 위한 src 생성 및 디렉토리 구조 변경

notion image
 
이후, 다음과 같이 컴포넌트의 참조 경로를 절대 경로로 바꾸었습니다.
import styled from '@emotion/styled'; import Button from 'src/components/Button'; const Message = styled.div` color: red; `; function index() { const message: string = 'HI'; return ( <> <Message css={{ background: 'blue' }}>{message}</Message> <Button width={96} height={32}> Click! </Button> </> ); } export default index;
notion image
잘 작동합니다!

추가 - @ 절대 경로 설정

저희는 src 뿐만 아니라 절대 경로를 커스터마이징 하였습니다.
이는 tsconfig.json에서 설정이 가능했는데요.
다행히도, next의 경우 CRA와 달리 계속해서 tsconfig를 초기화하지 않기에, 절대 경로를 직접 넣어주어도 문제가 발생하지 않았습니다. 따라서, 그대로 넣어주었습니다.
{ "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "jsxImportSource": "@emotion/react", "baseUrl": ".", "paths": { "@components/*": ["./src/components/*"], } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }
이후, storybook 역시 경로를 설정해주었습니다.
const path = require('path'); const toPath = (_path) => path.join(process.cwd(), _path); module.exports = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], webpackFinal: async (config) => { return { ...config, resolve: { ...config.resolve, alias: { ...config.resolve.alias, '@emotion/core': toPath('node_modules/@emotion/react'), '@emotion/styled': toPath('node_modules/@emotion/styled'), 'emotion-theming': toPath('node_modules/@emotion/react'), '@components': toPath('src/components'), }, }, }; }, };
다시 절대 경로를 정확히 인식하는지 테스트를 해봅니다.

src/stories/Button.stories.tsx

import React from 'react'; import Button, { ButtonProps } from '@components/Button'; export default { title: 'component/Button', component: Button, argTypes: { width: { defaultValue: 96, control: { type: 'range', min: 8, max: 500, step: 2 }, }, height: { defaultValue: 32, control: { type: 'range', min: 8, max: 500, step: 2 }, }, children: { type: 'text', defaultValue: 'Click!', }, }, }; const Template = (args: ButtonProps) => <Button {...args} />; export const Default = Template.bind({});
yarn storybook
notion image
잘 작동합니다. 😃
 

StyleLint

StyleLint는 초기 설정에 있어 가장 문제를 겪었던 라이브러리였습니다. 저희가 프로젝트를 시작하기 1달 전에 14버전으로 업그레이드가 되었는데요. 따라서 syntax에 대한 정형화된 이슈 논의가 부족하여 직접적으로 문제에 부딪혀야 했습니다.
💡
Q. 잠깐! 아직 1달밖에 되지 않았는데, 너무 최신 패키지 아닐까요? A. 그렇지만, 앞으로 발생할 패키지의 안정성을 최대화하기 위해 내린 선택입니다. 그 근거는, 핵심 extension인 Stylelint가 앞으로 14버전을 기준으로 지원할 거라, 나머지 패키지에서는 오류가 발생할 수 있다고 선언했기 때문입니다.
vscode-stylelint 1.x expects to use Stylelint 14 at minimum. Usage with prior versions of Stylelint is no longer supported. While older versions may continue to work for a while, you may encounter unexpected behaviour. You should upgrade your copy of Stylelint to version 14 or later for the best experience. The syntax and configOverrides options have been removed from Stylelint 14 and this extension. See the following section for information on how to use different syntaxes.

기본 설치

따라서 다음을 설치해줍니다.
"stylelint": "^14.1.0", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recess-order": "^3.0.0", "stylelint-config-standard-scss": "^3.0.0", "stylelint-order": "^5.0.0", "stylelint-prettier": "^2.0.0",

세부 패키지 설명

  • stylelint-config-prettier stylelint-prettier
    • 프리티어와 호환이 가능하도록 설정해줍니다.
      이때 styelint-config-prettier을 통해 모든 형식 지정 관련 Stylelint 규칙을 비활성화 할 수 있습니다 .
  • stylelint-config-standard-scss
    • 스타일린트가 scss 구문을 해석할 수 있도록 정보를 제공합니다. stylelint-config-sass-guideline과 함께 고민을 하였는데, 결과적으로 stylelint의 v14에서는 공식적으로 stylelint-config-standard-scss를 통해 진행하라고 권장하는 분위기었습니다.
      따라서 추후 업그레이드 시, 보다 안정적으로 업데이트가 가능할 것으로 예상 가능하여 택하였습니다. 기본적으로 stylelint-scss를 내장하고 있습니다.
  • stylelint-order
    • 스타일린트의 순서를 지정할 수 있도록 합니다.
  • 'stylelint-config-recess-order'
    • stylelint-order을 위한 이미 세팅된 패키지는 여럿 있었습니다. 그중 다음 기준을 통해 확장할 order convention package를 골랐습니다.
      1. 꾸준히 커뮤니티가 확장되고 있는가
        1. 커뮤니티가 확장이 된다는 것은, 앞으로 문제가 발생 시마다 논의가 활성화될 것이므로 좋다고 판단했습니다.
      1. 업데이트가 지속적으로 진행되고 있는가
        1. 예컨대 후보 중 하나였던 stylelint-config-concentric-order의 경우 수 년간 업데이트 되지 않고 있습니다. 지속적으로 발생하는 오류를 업데이트한다는 것은 굉장히 매력적이었습니다.

PostCSS Syntax Plugin 설치

여기에서 막혀서 꽤 많은 시간을 할애해야 했습니다. 바로, PostCSS와 StyleLint가 충돌하는 문제점이 발생한 것이었는데요.
정확히 말하자면, PostCSS에서는 정상동작하던 게, StyleLint에서는 scss 구문을 이해하지 못한다고 말하고 있었습니다.
해당 문서를 살펴 보니 Stylelint v14로 바뀌면서 가장 크게 바뀐 것은, automatic inferral of syntax을 자동으로 해주던 기능이 제거되었다는 것이었습니다.
따라서 이제부터는 우리가 직접 Syntax를 무엇으로 린트할 건지를 설정해줘야 했습니다.
If you use Stylelint to lint anything other than CSS files, you will need to install and configure these syntaxes. We recommend extending a shared config that includes the appropriate PostCSS syntax for you. For example, if you use Stylelint to lint SCSS, you can extend the stylelint-config-standard-scss shared config.

@stylelint/postcss-css-in-js

기본적으로 css-in-js의 구문을 파싱해줍니다. 현재 대체재가 없는 것으로 확인되어, 필수적으로 설치해야 합니다.
 
yarn add -D @stylelint/postcss-css-in-js
 

.stylelintrc.js

module.exports = { customSyntax: '@stylelint/postcss-css-in-js', extends: [ 'stylelint-config-standard-scss', 'stylelint-config-recess-order', 'stylelint-prettier/recommended', ], plugins: ['stylelint-scss', 'stylelint-order'], };
 
stylelint-prettier/recommended는 stylelint-config-prettier와 stylelint-plugin-prettier을 합친 옵션입니다.
 
실제로 stylelint-plugin-prettier에서는 recommended 옵션을 다음과 같이 설명하고 있습니다.
  • stylelint-plugin-prettier플러그인을 활성화합니다 .
  • prettier/prettier규칙을 활성화합니다 .
  • stylelint-config-prettier구성을 확장합니다 .
 

.stylelintignore 적용

스타일린트는 루트 경로 내에 존재하는 모든 디렉토리를 검사합니다. 따라서 검사를 무시할 부분을 따로 설정해주어야 합니다. 우리는 node_modules를 무시하고 검사합시다.

.stylelintignore

node_modules/**/*
 

package.json 린트 검사 구문 설정

이제는 다음을 통해 린트 검사를 자동으로 수행할 수 있게 되었습니다. 만약 오류가 발생한다면, 이후 나오는 .vscode까지 진행해주시기를 바랍니다.
  • package.json → "scripts"
    • "stylelint:fix": "stylelint --fix src",
 

.vscode

팀끼리 에디터 환경 설정하는 것 역시 중요한 부분입니다. 이유는, 각자의 환경에 따라 린터 및 포맷터의 장점이 사라지기도 하며, 알 수 없는 오류가 발생할 수 있기 때문입니다.
따라서 다음과 같이 모든 린터 및 포맷터가 정상적으로 동작할 수 있도록 정의하였습니다.
{ "eslint.format.enable": true, "editor.formatOnSave": false, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll.stylelint": true, }, "stylelint.enable": true, "css.validate": false, "less.validate": false, "scss.validate": false, "files.insertFinalNewline": true, "stylelint.validate": ["typescriptreact"], "stylelint.customSyntax": "@stylelint/postcss-css-in-js" }

참고 자료

https://nextjs.org/docs/basic-features/typescript
https://emotion.sh/docs/css-prop#jsx-pragma
https://github.com/prettier/eslint-config-prettier
https://www.npmjs.com/package/eslint-plugin-prettier
https://storybook.js.org/docs/react/get-started/install
https://github.com/emotion-js/emotion/issues/1249
https://github.com/storybookjs/storybook/issues/13277
https://nextjs.org/docs/advanced-features/customizing-babel-config
https://stylelint.io/migration-guide/to-14/
https://github.com/prettier/stylelint-prettier
https://github.com/stylelint/postcss-css-in-js/issues/225