[Next.js] Next.js + TypeScript + styled-components + Redux 프로젝트 시작하기 ( + Boilerplate 만들기 )

하영·2022년 7월 27일
9

Next.js

목록 보기
1/2
post-thumbnail

0. 들어가며

보통 새로운 프로젝트를 시작하면 로컬에서 작업환경을 구성하고, 새로 생성한 RepositoryPush하여 시작 준비를 마친다. 하지만 사용하는 라이브러리는 정해져있고, 설치 방법도 다 알고 있는 상황에서 매번 새롭게 세팅하려니 생각보다 시간을 잡아먹어서 묘한 불편함이 있었다.

특히 Next.js 같은 경우는 세팅 과정에 꽤나 많은 시간을 쏟아야 하기에 ( 아직 초보라 초기화 과정도 다 알아보고 해야 한다 ... ㅜ ) 초기화 과정을 글로 적어두고 매번 따라하면 좋을 것 같다! 생각하고 이 글을 쓰려 했다.

조금 검색해보니, Boilerplate 라고 하는 생소한 키워드로 설치과정을 설명하고 있었다. 보일러플레이트? 가 무엇인지 검색해보니,

최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드.

가장 쉬운 예로, React.js의 작업 환경을 구성할 때 사용하는 CRA(Create-React-App) 도 React를 사용하기 위해 미리 세팅된 파일을 사용하여 프로젝트를 구성하는 것이다!

아하! 그렇다면 자주 반복되는 설정을 template으로 구성하여 bolierplate code로 만들어두면 매번 가져다 쓰기 쉽겠구나!

그렇기에, 이 글에서는 Next.js를 사용한 프로젝트를 시작하면서, 추가 필요한 세팅들까지 작업한 코드를 boilerplate code로 만들어 볼 것이다! 코드를 만든 후, Github에 template repository로 만들면 사용하기 쉽다고 하니, 세팅을 마친 후 진행해보겠다.

세팅은 완전 바닥부터 시작하겠습니다!


1. yarn 설치하기

보통 라이브러리를 관리하는 패키지 매니저로 npm (node package manager) 를 많이 쓰는데, 내가 굳이 yarn을 사용하는 이유는 속도가 조금 더 빠르며, 보안성이 좋기 때문이다. npm에서 조금 발전된 것이 yarn이니... package manager에 대한 개념이 있다면 넘어가자! Node 설치는 기본으로 되어있어야 한다. LTS가 아닌, 가장 최신 버전으로 OS사양에 맞게 설치해주면 된다.

$ npm install -g yarn

설치 후 버전 확인

$ yarn --version


잘 설치되었다!


2. Next.js + TypeScript 프로젝트 생성

2.1 프로젝트 생성

프로젝트를 생성하고자 하는 위치에서 vscode를 실행하고 새 터미널을 열어주었다.

터미널에서 아래와 같이 명령어를 입력하면 타입스크립트와 함께 Next.js 프로젝트가 생성된다.

$ yarn create next-app --typescript [프로젝트 이름]

설치 완료 !!! 우와아아아앙 이제 시작....

2.2 폴더 구조

설치를 완료하였으니 생성한 프로젝트로 이동합시다.

$ cd [프로젝트 이름]

프로젝트 생성시 최초 파일 구조

생성된 파일을 보면 기존 React보다는 꽤나 낯선 구조다. 여기에서 효과적인 Next.js 프로젝트를 위해 내가 자주 사용하는 파일 구조를 추가해보겠다!

Next.js 개발에 최적화된 파일구조

components, hooks, types, utils, .env.local 등이 추가되었다! 기존에 있던 파일들까지 포함하여 파일구조를 설명하자면.. 위에서 순서대로

  • components - 컴포넌트를 관리
    • common - layout 또는 pages에서 자주 사용되는 공용 컴포넌트들
      • ex. CustomButton, CustomInputField 등...
    • layout - pages에 적용할 layout들을 관리하는 폴더
      • 제작한 layout은 pages에서 용도에 맞게 적용할 수 있다.
  • hooks - 용도에 맞는 custom hook을 관리
    • useInput, useCheck, useInterval 등...
  • pages - 페이지 구성을 담당하는 컴포넌트, 폴더 구조로 url을 결정
    • _app.tsx 파일은 기존 react에서의 index.js 와 같은 기능을 하는 파일
    • index.tsx 파일은 기존 react에서의 App.js 와 같은 기능을 하는 파일
  • public - 정적 파일을 관리
    • img, video, font 등...
  • styles - 전역 스타일 관리
    • styled-components를 사용하면 잘 사용하지 않음..
  • types - 자주 사용하는 타입 interface를 관리
  • utils - custom axios 관리, cookie 관리 등 기능 파일

파일구조까지 세팅되었다면 제대로 실행되는지 확인해본다.

$ yarn dev

우와 실행된다!


3. ESLint 및 Prettier 설정

react나 next.js 는 개발자 마다 폴더 구조가 천차만별인데, 코드 개발 스타일은 더더욱 다를 것이다. 그렇기에 프로젝트마다 코드 정리 규칙을 지정하여 다양한 개발자가 하나의 프로젝트를 개발하더라도 코드 스타일은 통일되도록 하는 것이 좋을 것이다! 이를 돕는 것이 eslintprettier 이다.

  • ESLint는 자바스크립트 문법에서 에러를 표시해주는 도구이다. 자신이 원하는 대로 규칙을 정할 수 있으며, 그 규칙에 부합하지 않으면 오류를 출력한다.
  • Prettier는 코드 정리 규칙을 설정하여 규칙에 맞게 자동으로 코드를 정렬하는 플러그인이다. 같은 프로젝트에 한해서 코드 스타일을 통일 할 수 있어 도움이 된다.

우선, vscode extension에서 ESLint, prettier-code formatter, stylelint를 설치한다. 설치과정은 생략한다!

3.1 Prettier 설정

prettier-code formatter 를 설치했다면, 위 메뉴바에서 파일 > 기본설정 > 설정으로 들어가서 아래 사진과 같은 것을 찾아 Default FormatterPrettier - Code formatter 로 설정한다. 그리고 필요하다면 자동저장을 켜준다.

그리고, 프로젝트의 root 디렉터리에 .prettierrc 파일을 생성한 후 아래 코드를 작성하여 저장한다. 출처

{
  "singleQuote": true, // 문자열에 홑따옴표 사용(')
  "semi": true, // 코드는 반드시 세미콜론으로 끝나야 함
  "useTabs": false, // tab대신 space를 사용
  "tabWidth": 2, // 1tab (들여쓰기) 에 space 2칸을 사용
  "trailingComma": "all", // 객체나 배열에서 맨 마지막 요소에 쉼표(,) 를 붙임
  "printWidth": 120, // 한줄이 120 자를 넘지 않음
  "arrowParens": "always" // 화살표 함수에서 단일 파라미터에 괄호를 붙임
  // const fn = (x) => (y) => x + y
}

아래와 같이 설정해두면 파일을 저장할 때 마다 위의 규칙에 코드가 맞춰진다. 저장 및 붙여넣기 할때 규칙에 맞춰지게 하려면 아래 사진과 같이 설정해주어야 한다.

3.2 ESLint 설정

ESLintstylelint를 설치했다면 아래와 같이 입력하여 ESLint 를 설치한다.

$ yarn add eslint --dev 

여기서 --dev속성은 개발 존속성으로 설치하겠다는 것이다. 프로젝트 빌드파일에는 포함되지 않게 한다.

아래 명령어를 입력하여 ESLint 초기화 및 규칙을 설정해준다.

$ yarn run eslint --init
√ How would you like to use ESLint? // eslint 사용 목적   
√ What type of modules does your project use? // 모듈 타입
√ Which framework does your project use? // 프레임워크
√ Does your project use TypeScript? // 타입스크립트 사용 유무
√ Where does your code run? // 코드 동작 환경
√ What format do you want your config file to be in? // eslint 설정 포맷 

위 대로 안해도 되니, 입맛에 맞게 설정하면 된다!

설정이 완료되면, 설정 포맷에 따라 `.eslintrc.js` 또는 `.eslintrc.json` 파일이 생성된다. 가장 많이 사용하는 포맷은 Airbnb 규칙 인데,

를 참고하여 필요한 사람은 꼼꼼히 설치하면 된다. 간단히 하려면 아래를 따라하면 된다.

$ yarn add --save-dev eslint-config-airbnb-base eslint-plugin-import 

.eslintrc.json 파일 수정

{
  ...
  "extends": [
    "airbnb-base", // 추가
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  ...
}

4. styled-components 구축하기

styled-components는 기존 react 프로젝트에서도 많이 사용하던 라이브러리다.

Vue와 비슷하게, 파일 하나에서 컴포넌트 스타일을 관리할 수 있으며, scss 문법을 사용하여 다양한 컴포넌트 스타일링이 가능하다.

styled-components를 사용하는 이유에 대해서는 추후 보강하고, 사용방법은 공식문서를 참고하면 좋을 듯 하다. 😀

4.1 styled-components 설치

styled-components 설치하자. 하지만, TypeScript 프로젝트 에서는 타입을 지정해주어야 하기 때문에, 타입을 포함하여 설치해주어야 한다. 아니면 제대로 작동하지 않아요!

$ yarn add styled-components
$ yarn add --dev @types/styled-components babel-plugin-styled-components
  • 여기에서 babel-plugin-styled-components는 컴포넌트의 이름을 해쉬 앞에 접두사로 붙여서 컴포넌트를 알아보기 쉽게 해 디버그를 돕는 라이브러리이다.
  • babel-plugin-styled-components@types/styled-components 는 개발 존속성으로 설치해주면 된다.

4.2 깜빡거리는 현상 해결

react에서는 styled-components를 그냥 설치해서 사용하면 되지만, styled-components는 css-in-js 이기에 브라우저 생성 이후 적용된다. 하지만 Next.js는 SSR(Server Side Rendering)으로 작동하기 때문에, 브라우저 생성 이전에 렌더링이 일어나게 되고, 그 이후에 브라우저가 렌더링되면 layout이 적용된다. 하지만 이 상황을 놓고 보면 style이 적용되기 전에 페이지 렌더링이 일어나는것이기에, 깜빡거리는 현상이 발생하게 된다.

이를 차근차근 해결해보자.

4.2.1 .babel.rc 작성

root 디렉터리에 .babelrc 파일을 생성하고 아래와 같이 코드를 작성해준다.

{
  "presets": ["next/babel"],
  "plugins": [
    [
      "babel-plugin-styled-components",
      {
        "ssr": true, //generate된 style이 server-side에서 적용되도록 함
		"fileName": true, // 코드가 포함된 파일명을 알려줌
        "displayName": true, // 클래스명에 해당 스타일 정보 추가
        "preprocess": false, // 실험 기능 이므로 꺼줌
		"pure": true // 사용하지 않은 속성 제거
      }
    ]
  ]
}

4.2.2 _document.tsx 파일 생성

pages폴더에 _document.tsx 파일을 생성하고 아래 내용을 채워넣는다. eslint에서 airbnb를 설치하지 않았다면 eslint오류로 빨간줄이 막 생길 수 있다. airbnb를 반드시 설치해 주어야 하는 듯 하다 !

아래 코드는 server-side에서 styled-components들의 style을 가져와 <style>태그로 변환한 후 html document에 삽입하는 과정을 나타낸다.

import Document, {
	Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
} from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });
      const initialProps = await Document.getInitialProps(ctx);

      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

여기까지는 가장 기본적인 Next.js 프로젝트 구성 단계이다. 전역관리 라이브러리에 따라 Redux를 사용하지 않을 수도 있고, 통신 라이브러리로 axios를 사용하지 않을 수도 있기에 이 이후로는 필요한 사람만 따라오면 될 듯 하고 필요 없는 사람은 바로 template 만들기로 넘어가면 될 듯 하다!

이렇게 하면 Next.js + TypeScript + styled-components 기본 설정 끝!

5. Redux 설정

가장 범용적으로 사용하는 전역 관리 라이브러리는 Redux 이다. 원래 Redux는 참 어려운 건데 .. 요즘 Redux를 편리하게 사용하기 위해 여러 미들웨어와 라이브러리가 개발되었다! 이를 설치만 간단히 해보도록 할 것이며, 자세한 사용법 및 환경은 따로 포스팅 예정이다. (에러가 많이 나서 애먹었다.. ㅜ)

Redux를 사용할땐 Reducers, Actions, Types로 나누어 관리하는 경우가 많다. 하지만 하나의 기능을 개발하기 위해 ReducersActions, Types를 모두 수정해야하여 불편함이 있다. 그렇기에, 하나의 파일에 한가지의 기능을 담당하도록 하는 Ducks Pattern을 사용하여 redux 파일 구조를 구성해보고자 한다.

하나의 파일에 reducer와 action type이 모두 들어가게 된다.
하나의 기능을 수정하거나 추가할 때 한 파일만 수정하면 되어 편리하다!

5.1 기본 설치 및 준비

우선, redux를 설치하자!

$ yarn add redux react-redux

그리고 Next.js(SSR) 에서 Redux를 사용하려면 따로 추가 설정을 해주어야 한다. 그 설정을 도와주는 라이브러리인 next-redux-wrapper 를 추가 설치한다.

$ yarn add next-redux-wrapper

그리고 redux를 편리하게 사용할 수 있게끔 도와주는 라이브러리인 redux-toolkit도 추가로 설치한다. 자세한 사용방법은 따로 다루고, 이번에는 store를 생성하는 부분만 다룰 예정이다

$ yarn add @reduxjs/toolkit

지금은 기능 파일은 따로 생성하지 않기에, root 디렉터리에 store 폴더를 만든 후 하위에 index.ts 파일만 생성해보도록 한다.

5.2 SSR 환경에서 Redux가 작동하도록 index.ts 작성 with redux-toolkit

우선, 생성한 index.ts 파일에서 rootReducer 부터 생성해준다.

const rootReducer = (state: any, action: AnyAction): CombinedState<any> => {
  switch (action.type) {
    case HYDRATE: 
      return { ...state, ...action.payload };
    default: {
      const combinedReducer = combineReducers(리듀서 목록);
      return combinedReducer(state, action);
    }
  }
};

HYDRATEnext-redux-wrapper에서, AnyAction, combineReducers, CombinedStateredux에서 import 해온다.

리듀서 목록에는 따로 파일로 만든 리듀서를 import해온 것을 아래와 같은 형태로 나열하면 된다. {user: userReducer, modal:modalReducer}, .... 이렇게 하면 useSelector로 접근할 때 state.user...state.modal... 로 접근할 수 있다.

HYDRATE 란 Server Side 단에서 렌더링 된 정적 페이지와 번들링된 JS파일을 클라이언트에게 보낸 뒤, 클라이언트 단에서 HTML 코드와 React인 JS코드를 서로 매칭 시키는 과정을 말한다.

이 과정을 redux측면에서 간단히 설명하자면, Next.js는 기본 동작 방식이 SSR 이기 때문에 Server Side Store와 Client Side Store가 각각 생성된다. 이를 합쳐주기 위해 root Reducer 단계에서 HYDRATE 라는 case를 잡아 처리하게 된다

다음은, redux-toolkitconfigureStore를 사용하여 store를 생성한다. 위에서 작성하던 코드 아래에 바로 작성하면 된다.

export const store = configureStore({
  reducer: rootReducer, // 위에서 만든 persistReducer를 대입
  devTools: process.env.NODE_ENV !== 'production', // redux devTool을 보일건지 말건지에 대한 유무
});

configureStore@reduxjs/toolkit 에서 import 해오면 된다.

다음은, 아래 3줄 코드를 추가한다.

const makeStore: MakeStore<EnhancedStore> = () => store;

export const wrapper = createWrapper<Store>(makeStore, { debug: process.env.NODE_ENV !== 'production' });

export type RootState = ReturnType<typeof rootReducer>;
  • makeStore라는 함수를 next-redux-wrapper 에서 import 해오고, EnhancedStore라는 타입을 @reduxjs/toolkit import 해온다. 스토어를 생성하는 함수를 만든다.
  • next-redux-wrapper 에서 제공하는 createWrapper 를 이용하여 위에서 만든 store를 SSR, CSR 모두 제공하기 위한 wrapper를 만든다. 만든 것을 export한다.
  • rootState도 정의하여 export한다.

5.3 예시 파일 만들기 with Ducks Pattern

ducks pattern에서는 action type/actions/reducers 를 하나의 파일로 관리한다. 참 편리한 구조인 듯 하다. 하지만 사용 방법을 생각보다 익히기가 어려웠다. 그렇기에 store/user.ts 라는 파일을 가이드 라인 파일로 사용하려 한다. 자세한 내용은 따로 하고, 가이드라인만 저장하였다.

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// user 스토어 타입 정의
interface UserTypes {
  userID: string;
  userName: string;
}

// user스토어의 초기값을 설정
const initialState: UserTypes = {
  userID: '',
  userName: '',
};

// ducks 패턴을 지원하기 위해 나온 함수가 createSlice.
const userSlice = createSlice({
  name: 'user', // 해당 모듈의 이름. store.user 형식으로 추후 접근
  initialState,
  reducers: {
    saveUserAction: (
      state: UserTypes,
      action: PayloadAction<{ userID: string; userName: string;}>,
    ) => {
      const { userID, userName } = action.payload;
      state.userID = userID;
      state.userName = userName;
    },
  },
});

export const { saveUserAction } = userSlice.actions;
export default userSlice.reducer;

5.4 _app.js에서 Next.js에 store주입

위의 index.ts 에서 만든 Wrapper를 사용하여 Next.js 에서 store에 접근 할 수 있도록 한다.
pages/_app.tsx 에 가서 아래와 같이 코드를 수정해준다.

import type { AppProps } from 'next/app'
import { wrapper } from 'store'; // 절대 경로 설정 필요 

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

export default wrapper.withRedux(MyApp);

이렇게 하면 SSR 단계, CSR단계 모두 store에 접근이 가능하다!

5.4.1 import 절대 경로 설정 방법은?

import 경로를 상대경로에서 절대 경로로 변경하려면 tsconfig.json 파일을 수정해주면 된다.

{
  "compilerOptions": {
    ...
    "baseUrl": "./", // 추가
  },
  ...
}
이렇게 하면 Redux 설정 끝!

6. Axios 설정

axios는 REST통신을 쉽게 하도록 도와주는 비동기 통신 라이브러리이다. axios instance를 만들어 header 설정이나 에러 상황 등을 처리할 수 있다.

Axios는 브라우저, Node.js를 위해서 만들어진 Promise 기반 HTTP 비동기 통신 라이브러리

백엔드와 통신을 위해서 사용하고, 프레임워크에서 ajax를 구현할 때는 axios를 주로 사용한다. http 요청과 응답을 json형태로 자동으로 변경해준다. 브라우저 내장인 fetch 보다는 조금 더 디테일하게 통신에 제약을 걸 수 있어 확장성 면에서 봤을때, axios를 사용하면 좋다.
출처: https://inpa.tistory.com/entry/AXIOS-%F0%9F%93%9A-%EC%84%A4%EC%B9%98-%EC%82%AC%EC%9A%A9

axios에 대해서 자세히 알고자 하면 공식문서 를 참고하자.

이 단계에서는 간단히 axios를 설치하고 custom axios instance 를 만들어 두는 정도로만 할 예정이다.

먼저, axios를 설치한다

$ yarn add axios

이후, utils 아래에 customAxios.ts 라는 파일을 만들어주고, 사용할 axios instance를 생성해준다. axios 객체를 생성한 후, 내장 함수를 통해 헤더를 설정하거나 오류 처리를

import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';

const instance = axios.create({
  baseURL: process.env.NODE_ENV === 'development' ? [개발할 때 사용할 url] : [배포할 때 사용할 url],
  timeout: 10000, // ms 단위
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance;

axios 객체는 axios 라이브러리에서 import 해오며, 타입스크립트 지원을 위한 AxiosError , AxiosRequestConfig, AxiosResponse 등 은 필요할 때 가져오면 된다. 예시를 위해 적어두었다.

  • baseURL : 통신의 기본이 될 주소다. 만들어진 axios객체를 사용하여 통신을 할 땐 ip나 도메인 같은 baseURL은 생략하고 /login 와 같이 사용하면 된다
  • timeout : ms가 기준이며, 해당 시간이 지나도 응답이 없다면 시간초과 에러를 발생시킨다
  • headers : 통신에 사용할 헤더를 설정하는 부분이다.

이렇게 설정한 후 export 해주면 axios통신이 필요한 곳에서

import axios from 'utils/customAxios'
axios.get('/login').....

해주면 된다.

이렇게 하면 Axios 설정 끝!


7. Git Template 만들기

이제 프로젝트 세팅을 마쳤으니 사용하기 쉽게 만들 단계이다. Github에 추가된 기능 중에는 저장소를 Template화 하여 다른 프로젝트를 쉽게 만들 수 있도록 도와주는 기능이 있다. 지금 까지 만든 Boilerplate 코드를 template 화 해보겠다.

git Repository를 생성하는 방법은 알고 계실 것이라 생각하고, 따로 설명하지 않겠습니다!

git repository를 생성할 때 README.md 파일을 따로 생성하지 않게끔 체크박스를 해제하고 아무 파일이 없도록 한다.

작업 중인 프로젝트 root 디렉터리에서 아래와 같이 입력하면 된다.

$ git remote add origin [git repository 주소] // repo 연결
$ git remote -v // 버전 확인
$ git add . // 모든 파일 stage
$ git status // 파일이 잘 올라갔는지 확인
$ git commit -m [commit message] // 간단한 커밋 메세지 추가 
$ git push origin main // main에 push 

만약에 실수로 readme 파일을 생성했다면 push할 때 -f 옵션을 사용하면 overwrite 된다

중간에 문제가 있어서 커밋이 2개가 되어 버렸지만.. 잘 업로드 되었다!

다음, template repo로 만들기 위해서는,


repository의 settings에서 Template repository라고 되어있는 곳의 체크박스에 체크를 해주면 된다.

끝이다 ! 만들어진 template을 이용해 새 repository를 만들고자 할 땐

Repo 이름 옆에 Public template 라는 뱃지가 생겼고 , Use this template 라는 버튼이 생겼다. Use this template 버튼을 누르면 템플릿 사용이 가능하다 !!! 우왕

완성 코드 : https://github.com/hayoung474/boilerplate-nextjs


8. 마치며

Next.js 프로젝트를 구축하기 위해 여러 문서를 찾아보고 정보의 바다에서 헤매었지만, 역시 가장 좋은 것은 손에 익고 기억하게끔 직접 정리하면서 나만의 방법을 찾는 것이었다.

TypeScript 사용과 SSR 환경이라는 것은 기존의 React와는 큰 차이점이 있기 때문에, 그냥 코드를 복사 붙여넣기 하는 수준에서의 세팅은 전혀 Next.js 이해에 도움이 되지 않았다. 이 글을 작성하면서 이런 코드가 작성되는 이유와 원리를 자세히 알고 잘못 알았던 부분을 다시 고칠 수 있어서 정말 도움이 되었다. 그렇기에 꽤나 긴 글이 나와 버렸다 ....

물론 아직 저도 초보이기에 모르는 것이 투성이입니다. 하지만 지적과 개선 사항은 언제나 환영입니다! 모르는 것이 있으시다면 질문도 환영입니다 ㅎㅎ

여러분의 Next 구축에 도움이 되셨으면 좋겠습니다

읽어주셔서 감사합니다 😊

출처

profile
maker를 넘어 solver를 지향합니다.

1개의 댓글

레이아웃이 제대로 적용 안돼서 쩔쩔 매고 있었는데 너무나 감사합니다ㅠㅠ

답글 달기