NextJS 13 + Prettier + ESLint + Recoil 초기 세팅 하기

이규현·2023년 9월 22일
post-thumbnail

개요

기존 React 팀 프로젝트에서 SEO를 위해 NextJS로 마이그레이션을 시작했다. 빠른 개발을 위해 최대한 기존 프로젝트에서 사용하고 있던 라이브러리들을 유지하고자 하였고 이에 TypeScript, Tailwind CSS와 Recoil을 계속 쓰기로 결정을 했다.

React만 써오다 NextJS 13으로 바꾸려다 보니 참고 자료가 상대적으로 적었고 그렇기에 블로그 글보단 공식 문서를 주로 참고하여 세팅을 진행하였다.

설치

먼저 NextJS의 자동 설치를 위해서 공식문서에서 권장하는 방법인 create-next-app 명령어를 통해 설치를 진행하였다.

npx create-next-app@latest

설치 과정에서 다음과 같은 프롬프트가 나온다.

What is your project named? my-app // 프로젝트 이름 설정
Would you like to use TypeScript? No / Yes // TypeScript 사용 여부
Would you like to use ESLint? No / Yes // ESLint 사용 여부
Would you like to use Tailwind CSS? No / Yes // TailWind CSS
Would you like to use `src/` directory? No / Yes // src/ 폴더 사용 여부
Would you like to use App Router? (recommended) No / Yes // App Router 사용 여부
Would you like to customize the default import alias? No / Yes // 절대 경로 사용 여부
What import alias would you like configured? @/* // 절대 경로 기호 선택

나는 절대 경로를 제외한 모든 옵션에 Yes를 선택하였다.

선택을 완료하면 npm을 통해 다음과 같은 dependencies들과 함께 자동으로 설치된다.
TypeScript, Tailwind CSS, ESLint까지 명령어 하나로 함께 세팅 되니 정말 편했다.

설치가 완료된 뒤 바로 npm run dev 명령어를 실행하면 다음과 같이 NextJS 프로젝트가 잘 생성된 것을 확인할 수 있다!

이후엔 초기화를 위해 app 바로 밑에 있는 page.tsx를 비웠고 global.css에서 tailwind css 설정을 제외한 모든 스타일을 삭제했다.

폴더 구조 설계

React와 달리 NextJS App Routing에서는 폴더 구조가 곧 라우팅이기 때문에 미리 페이지 폴더 구조를 잡고 가면 좋겠다고 생각했다.
먼저 URL을 깔끔하게 유지하기 위해 REST API는 아니지만 참고하면 좋을 거 같아 REST API 설계 예시를 참고하여 폴더명 규칙을 정했다.

  • 동사보다는 명사를, 대문자보다는 소문자를 사용하여야 한다.
  • 언더바 대신 하이폰을 사용한다.
  • 마지막에 슬래시 (/)를 포함하지 않는다.
  • 파일확장자는 URI에 포함하지 않는다.

다음으론 app/ 디렉토리 구조를 설계했다. 기존 프로젝트에선 페이지들을 기능 별로 폴더로 묶었었는데 NextJS에서는 디렉토리 구조가 곧 라우팅이기에 기능별로 묶는것이 라우팅에 영향을 줄까봐 골치가 아팠다. 다행히 공식 문서를 뒤져보니 app/ 디렉토리 밑에서 라우팅에 영향을 주지 않으면서 폴더로 묶는 방법이 있었다. 폴더 이름을 소괄호로 감싸면 라우팅에 영향을 주지 않는다!!

./src/app
└── (user)
    └── login
        └── page.tsx

login 페이지에 접근할떄 (user)라는 폴더로 감싸고 있어도 폴더 명을 소괄호가 감싸기 때문에 /(user)/login이 아닌 /login으로 이동해야 한다.

또한 NextJS에선 동적 라우팅을 하고 싶으면 폴더명을 대괄호로 감싸면 된다. 그러면 대괄호로 감싸진 이름이 Path Parameter가 되고 App Router Props 객체에 params로 담겨 오게 된다.

// app/blog/[slug]/page.js
export default function Page({ params }: { params: { slug: string } }) {
  return <div>My Post: {params.slug}</div>
}

NextJS 공식문서 예제 : 폴더 명이 [slug]면 key는 slug, value는 Path Parameter인 객체가 Props 객체 안 params 안에 담겨온다.

다음은 정적 파일 폴더인데 NextJS에선 정적 파일들을 robot.txt나 favicon.ico같은 특별한 metadata 정적 파일이 아닌 이상 무조건 최상위 폴더에 있는 public 폴더 안에 넣어야 한다고 한다. 이에 기존에 쓰던 모든 정적 파일들을 ./public 폴더 안에 파일 유형별로 분리해서 넣었다.

.
├── public
│   ├── fonts
│   └── images
├── (...)

마지막으로 라우팅에 영향을 주지 않는 폴더들을 src 폴더 밑에 생성했는데, 컴포넌트들을 모아둘 components 폴더와 커스텀 훅들을 모으는 hooks 폴더, 기타 파일을 모아둘 utils 폴더를 생성하며 폴더 구조 설계를 마쳤다.

다음은 최종 설계한 폴더 구조이다.

.
├── .next
├── node_modules
├── public
│   ├── fonts
│   ├── images
│   └── lotties
├── src
│   ├── app
│   │   ├── (error)
│   │   │   └── error
│   │   │       └── page.tsx
│   │   ├── (read)
│   │   │   └── post
│   │   │       └── [title]
│   │   │           └── page.tsx
│   │   ├── (search)
│   │   │   ├── search-post
│   │   │   │   └── page.tsx
│   │   │   └── search-user
│   │   │       └── page.tsx
│   │   ├── (submit)
│   │   │   ├── modification
│   │   │   │   └── page.tsx
│   │   │   └── new-post
│   │   │       └── page.tsx
│   │   ├── (user)
│   │   │   ├── login
│   │   │   │   └── page.tsx
│   │   │   ├── profile
│   │   │   │   └── page.tsx
│   │   │   └── registration
│   │   │       └── page.tsx
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components
│   ├── hooks
│   └── utils
├── .eslint.json
├── .gitignore
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock

참고로 yarn.lock이 생겨 있는 것을 볼 수 있는데 프로젝트 팀원이 npm보다 yarn을 선호해 패키지 매니저를 yarn으로 바꾸게 되었다.
설치된 node modules 폴더와 package.lock.json을 지우고 yarn 명령어를 실행하여 바꿨다.

Prettier 세팅

Prettier를 세팅하기 위해 yarn 공식사이트에서 prettier를 검색하여 설치하였다.

Prettier는 code formatter기 때문에 개발과정에서만 사용하기위해 -d 옵션을 주어 dev dependency로 설치하였다.

yarn add prettier -d

다음은 프로젝트 최상위 폴더에 .prettierrc라는 설정파일을 다음과 같이 추가하였다.

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "arrowParens": "always"
}

규칙은 https://dev-yakuza.posstree.com/ko/react/nextjs/prettier/ 포스팅과 기존 React 프로젝트에서 쓰던 포맷팅을 합쳐서 정했다. 기존 React Project와 다른 옵션으로 "semi"옵션을 false로 준게 있는데 위 글에서 NextJS에선 semicolon을 안 붙인다고 해서 진짜 공식문서를 확인해보니 세미콜론이 없는 것을 확인하고 신기하였다. formatting 규칙은 개인 취향이지만 그래도 공식 문서를 따라가기로 결정했다..

마지막으로 package.json에 script를 추가하였다.

  "scripts": {
	...
    "format": "prettier --check --ignore-path .gitignore .",
    "format:fix": "prettier --write --ignore-path .gitignore ."
  },

--ignore-path 옵션으로 .gitignore 파일을 선택함으로써, Git으로 관리하지 않는 파일들은 검사하지 않도록 설정한 스크립트라고 한다.

ESLint 세팅

ESLint는 위에서create-next-app 명령어를 실행했을때 설치가 이미 되었었는데 설정파일의 내용이 다음과 같다.

{
  "extends": "next/core-web-vitals"
}

확장으로 next/core-web-vitals가 추가 된 것을 볼 수 있는데 이는 엄격한 Core Web Vitals rule-set과 ESLint의 NextJS의 기본 설정이 포함되어 있어 ESLint를 처음 설정하는 개발자에게 추천되는 옵션이라고 NextJS 공식문서에서 설명하고 있다.

이제 이 설정파일을 커스텀 하여 ESLint를 세팅하였다.

{
  "extends": [
    "next/core-web-vitals",
    "airbnb",
    "airbnb-typescript",
    "prettier"
  ],
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "rules": {
    "react/react-in-jsx-scope": "off"
  }
}

추가한 확장들을 설명하자면

  • airbnb : airbnb사에서 쓰고 있는 ESLint 규칙이 포함되어있는 확장이다. 다음 명령어로 설치할 수 있다.
yarn add eslint-config-airbnb -d
  • airbnb-typescript : airbnb 규칙을 typescript에서도 사용하기 위한 확장이다. typescript는 parsing이 필요하기 때문에 @typescript-eslint/eslint-plugin @typescript-eslint/parser 들과 함께 설치 해야 하며 "parserOptions"를 설정해야 한다.
yarn add eslint-config-airbnb-typescript
            @typescript-eslint/eslint-plugin@^6.0.0
            @typescript-eslint/parser@^6.0.0 -d
  • prettier : Prettier과의 충돌을 방지하는 확장이다. Prettier를 따로 사용하고 있기에 설정이 필요하였다. 다음 명령어로 설치할 수 있다.
yarn add eslint-config-prettier -d

또한 "react/react-in-jsx-scope" 규칙을 꺼버린 것을 볼 수 있다. 이는 jsx, tsx 파일에 React를 import하지 않으면 나타나는 에러인데 최신 React나 NextJS에서는 React를 import 하지 않아도 되기 때문에 규칙을 꺼버렸다.

나머지 규칙은 개발을 진행하며 설정하기로 하였다. 규칙 하나하나 회의를 통해 사용할지 말지 결정할 예정이다. 에러가 난다고 그냥 꺼버리면 ESLint를 사용하는 보람이 없으니까..

Recoil 세팅

먼저 Recoil을 사용하기 위해 설치를 진행하였다. 참고로 Recoil은 CSR에서만 사용할 수 있다.

yarn add recoil

다음으로 Recoil을 세팅하기위해 최상위 layout.tsx에 RecoilRoot를 씌우려고 했는데 문제가 발생했다.
자식 컴포넌트에서 Recoil을 사용하기 위해선 RecoilRoot로 부모 컴포넌트에서 자식을 둘러 싸야한다. (부모 컴포넌트에선 사용 불가)
기존 React에선 React-Router-dom을 사용하면 이런식으로 프로젝트 전체에 Recoil을 세팅할 수 있었다.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { RecoilRoot } from 'recoil';

function App() {
  return (
      <RecoilRoot>
        <Router>
            <Routes>
              <Route path="/" element={<MainPage />} />
              <Route path="/write" element={<WritingPage />} />
              <Route path="/*" element={<ErrorPage />} />
            </Routes>
        </Router>
      </RecoilRoot>
  );
}
export default App;

NextJS App routing에선 app/ 디렉토리 바로 밑에 있는 layout.tsx가 모든 다른 layout.tsx나 page.tsx등의 부모 컴포넌트로 동작하기 때문에 최상위 layout.tsx의 children을 RecoilRoot로 감싸면 세팅이 완료될 줄 알았다. 하지만 에러가 발생하였다.

import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { RecoilRoot } from 'recoil'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Graphy',
  description: 'Project Share platform',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RecoilRoot>{children}</RecoilRoot>
      </body>
    </html>
  )
}

보다시피 RecoilRoot는 Client Components에서만 사용할 수 있다고 한다. 그러면 최상위 layout.tsx를 Client Components로 만들면 되지 않냐고 생각할 수 있지만 이도 에러가 난다.

'use client'

import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { RecoilRoot } from 'recoil'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Graphy',
  description: 'Project Share platform',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RecoilRoot>{children}</RecoilRoot>
      </body>
    </html>
  )
}

SEO를 위한 Metadata가 선언 되어 있는 최상위 layout.tsx 파일은 Client Components로 쓸 수 없다.

이 문제를 해결하기 위해 많은 고민 끝에 해결방법을 떠올렸다. 바로 RecoilRoot를 추가하는 대신에 중간에 CSR 컴포넌트를 추가하기로 했다.

// recoilRootProvider.tsx
'use client'

import { RecoilRoot } from 'recoil'

export default function RecoilRootProvider({
  children,
}: {
  children: React.ReactNode
}) {
  return <RecoilRoot>{children}</RecoilRoot>
}
// app/layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import RecoilRootProvider from '../utils/recoilRootProvider'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Graphy',
  description: 'Project Share platform',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <RecoilRootProvider>{children}</RecoilRootProvider>
      </body>
    </html>
  )
}

이제 잘 작동한다!!

참고자료

https://nextjs.org/docs
https://yarnpkg.com/
https://dev-yakuza.posstree.com/ko/react/nextjs/prettier/

profile
Front-end Engineer를 목표로 달리고 있습니다

1개의 댓글

comment-user-thumbnail
2023년 9월 24일

이 글을 보고 프로젝트 세팅을 했어요~

답글 달기