구글 폼 만들기

Yunes·2023년 12월 23일
0

리액트스터디

목록 보기
18/18
post-thumbnail

eslint + prettier 설정

도입 근거 및 고민

우아한 테크코스 프리코스를 해보며 처음 도입을 해봤던 eslint 와 prettier의 조합은 일관성 있는 코딩컨벤션을 지킬 수 있었기에 그 이후 협업을 하거나 일관된 코드를 작성하고 싶어서 eslint 와 prettier 를 도입하게 되었다.

또한 react 를 사용하며 다른 컴포넌트나 react hook, theme, img, 라이브러리 등을 import 해서 사용할 때가 많은데 훅은 위로, theme 는 아래로 배치하는 것과 같이 import 하는 것들을 정리해주려는 목적도 있다.

또한 이것을 설정하면서 절대경로도 도입하게 되는데 상대경로로 import 하다보면 한도 없이 길어지는 경로를 간단하게 줄여서 표현할 수 있기에 프로젝트 초반에 항상 이 설정을 하고 시작하는 편이다.

ESLint 파헤치기

ESLint 는 어떻게 코드를 분석할까?

ESLint : 자바스크립트 코드를 정적 분석해 잠재적인 문제를 발견하고 나아가 수정까지 도와주는 도구

  1. 자바스크립트 코드를 문자열로 읽는다.
  2. 자바스크립트 코드를 분석할 수 있는 파서로 코드를 구조화
  3. 2번에서 구조화한 트리를 AST (Abstract Syntax Tree) 라 하며 이 구조화된 트리를 기준으로 각종 규칙과 대조
  4. 규칙과 대조했을 때 이를 위반한 코드를 알리거나 (report) 수정한다. (fix)

ESLint 가 사용하는 파서의 기본값은 espree 이다. 코드를 espree 로 분석하면 JSON 형태로 구조화된 결과를 얻을 수 있다.

espree : 코드 분석 도구.

  • 변수 / 함수 / 함수명이 무엇인지 / 코드의 정확한 위치 같은 세세한 정보도 분석해 알려준다.
  • 타입스크립트는 @typescript-eslint/typescript-estree 라는 espree 기반 파서가 있고 이를 통해 타입스크립트 코드를 분석해 구조화한다.

ESLint 규칙 : ESLint 가 espree 로 코드를 분석한 결과를 바탕으로 어떤 코드가 잘못된 코드고 어떻게 수정해야 할지 정하는 규칙
plugin : 특정한 ESLint 규칙의 모음

eslint-plugin & eslint-config

eslint-plugin

ESLint 규칙들을 모아놓은 패키지다.

리액트, import 같이 특정 프레임워크나 도메인과 관려된 규칙을 묶어서 제공하는 패키지

eslint-plugin-import 패키지

  • js 에서 다른 모듈을 불러오는 import 와 관련된 다양한 규칙을 제공한다.

eslint-plugin-react 패키지

  • JSX 배열에 키를 선언하지 않았다는 경고 메세지의 경우 이 패키지의 규칙중 react/jsx-key 가 경고를 보여준 것이다.

eslint-config

eslint-plugin 을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지

  • eslint-plugin, eslint-config 라는 접두사를 쓸 경우 뒤에 오는 단어는 반드시 한 단어로 구성해야 한다.
    ex) eslint-config-naver ( O ) / eslint-config-naver-financials ( X )
  • 특정 스코프가 앞에 붙는 것은 가능하다.
    ex) @titicaca/eslint-config-triple ( O ) / @titicaca/eslint-config-triple-rules ( X )

eslint-config-airbnb

  • 자바스크립트 프로젝트에 적용할 ESLint 중에서 가장 합리적일 수 있다.
  • 에어비앤비 개발자뿐만 아니라 수많은 개발자가 유지보수하고 있는 가장 유명한 eslint-config
  • 다른 유명한 config 인 eslint-config-google, eslint-config-naver 대비 압도적인 다운로드 수를 보인다.

@titicaca/triple-config-kit

  • 스타트업 개발사인 트리플에서 개발
  • 대부분의 eslint-configeslint-config-airbnb 를 기반으로 약간 룰을 수정해 배포하고 있으나 이 패키지는 자체적으로 정의한 규칙을 기반으로 운영
  • 외부로 제공되는 규칙에 대한 테스트 코드가 존재
  • CI/CD 환경, 카나리 배포 등 일반적인 npm 라이브러리 구축 및 관리를 위한 시스템이 잘 구축되어 있다.
  • 별도의 frontend 규칙을 제공해 Node.js 혹은 리액트 환경에 맞는 규칙을 적용할 수 있다.
  • ESLint 뿐 아니라 Prettier, Stylelint 를 각각 별도의 룰인 @titicaca/prettier-config-triple, @titicaca/stylelint-config-triple 로 모노레포를 만들어 관리중이라 각각 필요에 따라 설치해 사용가능

eslint-config-next

  • 리액트 기반 Next.js 프레임워크를 사용하고 있는 프로젝트에서 사용할 수 있는 eslint-config
  • 단순히 자바스크립트 코드를 정적으로 분석할 뿐 아니라 페이지, 컴포넌트에서 반환하는 JSX 구문 및 layout 에서 작성되어 있는 HTML 코드 또한 정적 분석 대상으로 분류해 제공한다.
    • 자바스크립트 코드에 대한 향상
    • 전체적인 Next.js 기반 웹 서비스의 성능 향상에 도움
    • 핵심 웹 지표 ( core web vitals ) 라고 하는 웹 서비스 성능에 영향을 미칠 수 있는 요소들을 분석해 제공

트리쉐이킹 : 번들러가 코드 어디에서도 사용하지 않는 코드 (dead code) 를 삭제해서 최종 번들 크기를 줄이는 과정을 의미한다. 나무의 나뭇잎을 털어낸다는 의미에서 유래했다.

만약 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable- 주석을 사용하면 된다. 특정 줄만 제외하거나 파일 전체를 제외하거나 특정 범위에 걸쳐 제외하는 것이 가능하다.

// 특정 줄만 제외
console.log('hello world') // eslint-disable-line no-console

// 다음 줄 제외
// eslint=disable-next-line no-console
console.log('hello world')

// 특정 여러 줄 제외
/* eslint-disable no-console */
console.log('hello world')
console.log('hello world')
/* eslint-disable no-console */

// 파일 전체에서 제외
/* eslint-disable no-console */
console.log('hello world')

eslint-disable-line no-exhaustive-deps

  • useEffect, useMemo 같이 의존 배열이 필요한 훅에 의존성 배열을 제대로 선언했는지 확인하는 역할을 하므로 개발시 의존성 배열이 너무 길어지거나 빈 배열을 넣어서 컴포넌트가 마운트되는 시점에 한 번만 강제로 실행되게 하고 싶을 때, 혹은 임의로 판단해 없어도 괜찮다고 생각될 때 사용한다. -> 단, 잠재적인 버그를 야기할 수 있다.1

세부사항

  1. 필요한 라이브러리 설치
npm i -D eslint prettier eslint-config-prettier eslint-plugi
n-prettier
  • eslint-config-prettier : prettier 를 eslint plugin 으로 추가하며 prettier 가 인식하는 코드 포맷 오류를 eslint 오류로 출력하도록 한다.
  • eslint-plugin-prettier : eslint 의 코드 포맷과 관련된 rule 중 prettier 와 충돌하는 부분을 비활성화할 수 있다.
  1. .prettierrc 설정
{
  "singleQuote": true,
  "semi": false,
  "printWidth": 80
}
  1. .eslintrc.json 설정
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "prettier", "import"],
  "extends": [
    "eslint:recommended",
    "airbnb",
    "prettier",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "linebreak-style": 0,
    "import/prefer-default-export": 0,
    "prettier/prettier": 0,
    "import/extensions": 0,
    "no-use-before-define": 0,
    "import/no-unresolved": 0,
    "import/no-extraneous-dependencies": 0,
    "no-shadow": 0,
    "react/prop-types": 0,
    "react/react-in-jsx-scope": "off",
    "react/function-component-definition": [
      2,
      {
        "namedComponents": "function-declaration",
        "unnamedComponents": "arrow-function"
      }
    ],
    "react/jsx-filename-extension": [
      2,
      { "extensions": [".js", ".jsx", ".ts", ".tsx"] }
    ],
    "jsx-a11y/no-noninteractive-element-interactions": 0,
    "react/jsx-props-no-spreading": "warn",
    "jsx-a11y/label-has-associated-control": [
      2,
      {
        "labelAttributes": ["htmlFor"]
      }
    ],
    "import/order": [
      "warn",
      {
        "groups": ["builtin", "external", ["parent", "sibling"], "index"],
        "newlines-between": "always",
        "alphabetize": { "order": "asc" }
      }
    ]
  }
}

import/order

groups 에 들어가는 세부 사항들에 대해 정리해봤다.

groups 엔 ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"] 가 올 수 있고 아래에 예시를 찾아두었다.

// 1. node "builtin" modules
import fs from 'fs';
import path from 'path';
// 2. "external" modules
import _ from 'lodash';
import chalk from 'chalk';
// 3. "internal" modules
// (if you have configured your path or webpack to handle your internal paths differently)
import foo from 'src/foo';
// 4. modules from a "parent" directory
import foo from '../foo';
import qux from '../../foo/qux';
// 5. "sibling" modules from the same or a sibling's directory
import bar from './bar';
import baz from './bar/baz';
// 6. "index" of the current directory
import main from './';
// 7. "object"-imports (only available in TypeScript)
import log = console.log;
// 8. "type" imports (only available in Flow and TypeScript)
import type { Foo } from 'foo';
  • groups 내에 명시하지 않은 모듈은 모두 동일한 순서를 지닌다.

    • builtin : 내장 모듈
    • external : 외부 라이브러리 모듈
    • internal : 내부 라이브러리 모듈, tsconfig
    • parent : 상대경로중 상위 디렉토리
    • sibling : 상대경로중 동일 디렉토리 혹은 하위 디렉토리
    • index : 현재 디렉토리
    • object : 객체 -> TypeScript 에서만 가능
    • type : 타입 -> TypeScript, Flow 에서 가능
  • newlines-between : always : 다른 그룹 사이에 한줄 띄고 같은 그룹 내에서는 사이에 빈 줄을 허용하지 않는다.

  • alphabetize : { order: asc } : 알파벳 오름차순 기준으로 정렬한다.

styled-components 설정

도입 근거 및 고민

스타일을 적용하는 다양한 방식중에서 styled-components 를 가장 애용중인데 bootstrap 이나 tailwind css 같은 경우 class 가 점점 길어지는 것이 가독성에 좋지 않다고 생각되어 보통 styled-components 를 활용하는 방식을 주로 사용하고 있다.

설치

npm i styled-components
npm i -D @types/styled-components

global theme, default theme

styled-components 의 createGlobalStyle 로 reset css 를 적용했고 그 외 테마는 DefaultTheme 를 설정해서 적용했다.

// GlobalStyle.tsx

import { createGlobalStyle } from "styled-components"

const GlobalStyle = createGlobalStyle`
  html, body, div, span, applet, object, iframe,
  h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  a, abbr, acronym, address, big, cite, code,
  del, dfn, em, img, ins, kbd, q, s, samp,
  small, strike, strong, sub, sup, tt, var,
  b, u, i, center,
  dl, dt, dd, ol, ul, li,
  fieldset, form, label, legend,
  table, caption, tbody, tfoot, thead, tr, th, td,
  article, aside, canvas, details, embed, 
  figure, figcaption, footer, header, hgroup, 
  menu, nav, output, ruby, section, summary,
  time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
  }
  /* HTML5 display-role reset for older browsers */
  article, aside, details, figcaption, figure, 
  footer, header, hgroup, menu, nav, section {
    display: block;
  }
  body {
    line-height: 1;
    background-color: #F0EBF8;
  }
  ol, ul {
    list-style: none;
  }
  blockquote, q {
    quotes: none;
  }
  blockquote:before, blockquote:after,
  q:before, q:after {
    content: '';
    content: none;
  }
  table {
    border-collapse: collapse;
    border-spacing: 0;
  }
`
export default GlobalStyle
// defaultTheme.ts

import { DefaultTheme } from "styled-components"

const defaultTheme: DefaultTheme = {
  colors: {
    WHITE: "#FFFFFF",
    PURPLE_LIGHT: "#F0EBF8",
    PURPLE_DARK: "#673AB7",
    FOCUS_BLUE: "#4285F4",
    GRAY: "#202124",
  },
}

export default defaultTheme

react-router-dom 설정

도입 근거 및 고민

react + redux 만으로 웹 페이지를 구성하려니 파일 시스템 구조를 따르는 next.js 와 달리 각 페이지별 라우팅을 따로 설정해줘야 한다. 이를 위해 react-router-dom 을 사용하려고 한다.

Router 방식 고민

React Router 에는 두가지 방식의 라우터 사용방법이 있다.

  1. BrowserRouter
import * as React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

const root = createRoot(document.getElementById("root"));

root.render(
  <BrowserRouter>
    {/* The rest of your app goes here */}
  </BrowserRouter>
);

//////

function App() {
  return (
    <BrowserRouter basename="/app">
      <Routes>
        <Route path="/" /> {/* 👈 Renders at /app/ */}
      </Routes>
    </BrowserRouter>
  );
}

BrowserRouter 는 clean URL 을 사용해서 브라우저 주소바의 현재 위치를 저장한다. 그리고 브라우저의 built-in history stack 을 사용해서 navigate 할 수 있게 한다.

  1. createBrowserRouter

React Router 는 createBrowserRouter 를 모든 React Router 웹 프로젝트에서 사용할 것을 추천하고 있다. 이는 URL 을 업데이트하고 history stack 을 관리하기 위해 DOM History API 를 사용한다.

또한 이를 사용해야 loader, action, fetcher 같은 v6.4 에서 새로 추가된 data API 를 사용할 수 있다.

  • 동적인 페이지에 적합
  • 검색엔진 최적화 (SEO)
  • github-pages 배포가 까다로움

두가지 방식중 어떤 방식을 사용할지 고민을 해본 결과 createBrowserRouter 를 사용하기로 했다. v5 까지는 BrowserRouter 를 주로 사용했으나 현재 v6 을 넘어 업데이트가 되고 있는 상황이기도 하고 React Router 에서 권하는 createBrowserRouter 를 쓰지 않을 이유가 없다고 생각이 들었다.

새 방식을 적용하는데 거부감이 적은 편이니 바로 사용해봤다.

useNavigate vs redirect 고민

createBrowserRouter 를 사용중이니 action 이나 loader 같은 data API 를 활용할 수 있다.

React Router 문서를 찾다보면 useNavigate 페이지에서 이것을 사용하지 말고 action 이나 loader 등을 통해 redirect 를 하라는 것을 권장하고 있다.

이 이유에 대해서 React Router 에 대해 찾아보며 정리한 페이지에 작성해두었다.

그러니 redirect 를 써야 할 것으로 생각되나 이 프로젝트는 구글 폼이 동작하는 것처럼 보이도록 하고 실제 데이터는 전역상태관리 라이브러리인 redux 에 저장해둘 생각이라 실제 data fetch 가 이루어지지 않을 것이다. (백엔드를 만들 생각이 없으니..)

그렇다면 그냥 useNavigate 를 쓰는 것이 맞나.. 하는 고민이 든다.

왜냐하면 loader 는 앱이 렌더되기 전에 실행된다면 action 은 특정 동작에 의해 수행된다. 이때 action 은 get 을 제외한 post, put, patch, delete 와 같이 non-get submission 을 요청할때 호출된다.

즉, 내가 원하는 것은 제출버튼을 클릭하는 동작 / 눈 모양을 클릭하는 동작등을 통해 다른 페이지로 navigate 를 해주고 싶은데 그 과정에서 HTTP 요청이 없을 것이다. 그러니 loader 나 action 을 사용하게 될까 하는 생각이 들어 일단 useNavigate 를 사용하려 한다.

설치

npm i react-router-dom
npm i -D @types/react-router-dom

사용법

import React from "react"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import { RouterProvider, createBrowserRouter } from "react-router-dom"
import { ThemeProvider } from "styled-components"

import Root from "./Root"
import { store } from "./app/store"
import { Form } from "./features/form/Form"
import { Preview } from "./features/preview/Preview"
import { Result } from "./features/result/Result"
import GlobalStyle from "./style/GlobalStyle.js"
import defaultTheme from "./style/defaultTheme.js"
import "./index.css"

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "form",
        element: <Form />,
      },
      {
        path: "preview",
        element: <Preview />,
      },
      {
        path: "result",
        element: <Result />,
      },
    ],
  },
])

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <ThemeProvider theme={defaultTheme}>
      <Provider store={store}>
        <GlobalStyle />
        <RouterProvider router={router} />
      </Provider>
    </ThemeProvider>
  </React.StrictMode>,
)

BrowserRouter 와 달리 App 을 사용하지 않고 RouterProvider 를 사용하며 children 을 사용할 경우 parent 에 Outlet 컴포넌트를 사용해 children 이 부모 컴포넌트중 어디에 들어가야 하는지 표시해야 한다.

  • RouterProvider : 모든 데이터 라우터 객체는 이 컴포넌트로 전달해야 앱을 렌더링할 수 있고 나머지 데이터 API 를 사용할 수 있다.

마주친 에러

createBrowserRouter 내에 들어가는 router 정보를 따로 파일로 분리하려 했는데 위와 같은 ts2749 에러가 발생했다.

찾다보니 이런 에러는 .tsx 로 파일을 만들어야 하는데 .ts 로 파일을 생성했을 경우 뜨는 에러라는 것을 알게 되었다.

.ts 와 .tsx 의 차이에 대해 자세히 알지 못하고 그저 컴포넌트를 사용할 때 tsx 같이 사용하는게 관례.. 라고 알고 있었는데 이번 기회를 삼아 차이에 대해 찾아봤다.

  • .ts : typescript 만 사용할 경우
  • .tsx : react component 와 같이 사용할 경우에 사용
    즉, 위의 경우 Root, Form, Preview, Result 같은 컴포넌트를 타입스크립트에 같이 사용하려 하는데 파일 확장자가 .ts 라서 에러가 뜬 것이었다.

Material UI 설정

도입 근거 및 고민

일단 시간이 굉장히 부족하다. 만들어져 있는 컴포넌트를 쉽게 가져다 쓸 수 있다면 기꺼이 가져다 쓸 생각이다. AntD 와 MUI 를 모두 사용해본 결과 Docs 는 MUI 가 더 잘되어 있는 것 같아 MUI 를 사용하려고 한다.

설치

npm install @mui/icons-material @mui/material @emotion/styled @emotion/react

사용법

MUI 사이트에 나온 코드를 그대로 import 해서 쓰면 된다.

color

import * as React from 'react';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';

export default function Color() {
  return (
    <Typography component="div" variant="body1">
      <Box sx={{ color: 'primary.main' }}>primary.main</Box>
      <Box sx={{ color: 'secondary.main' }}>secondary.main</Box>
      <Box sx={{ color: 'error.main' }}>error.main</Box>
      <Box sx={{ color: 'warning.main' }}>warning.main</Box>
      <Box sx={{ color: 'info.main' }}>info.main</Box>
      <Box sx={{ color: 'success.main' }}>success.main</Box>
      <Box sx={{ color: 'text.primary' }}>text.primary</Box>
      <Box sx={{ color: 'text.secondary' }}>text.secondary</Box>
      <Box sx={{ color: 'text.disabled' }}>text.disabled</Box>
    </Typography>
  );
}

font weight

import * as React from 'react';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';

export default function FontWeight() {
  return (
    <Typography component="div">
      <Box sx={{ fontWeight: 'light', m: 1 }}>Light</Box>
      <Box sx={{ fontWeight: 'regular', m: 1 }}>Regular</Box>
      <Box sx={{ fontWeight: 'medium', m: 1 }}>Medium</Box>
      <Box sx={{ fontWeight: 500, m: 1 }}>500</Box>
      <Box sx={{ fontWeight: 'bold', m: 1 }}>Bold</Box>
    </Typography>
  );
}

line height

import * as React from 'react';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';

export default function LineHeight() {
  return (
    <Typography component="div">
      <Box sx={{ lineHeight: 'normal', m: 1 }}>Normal height.</Box>
      <Box sx={{ lineHeight: 2, m: 1 }}>line-height: 2</Box>
    </Typography>
  );
}

마주한 에러

control-has-associated-label 은 button 내에 text label 이 있어야 한다는 규칙을 명시하는 패키지다.

그런데 나같이 내부에 텍스트를 넣기 싫고 아이콘을 넣고자 할 경우 텍스트 대신 aria-label attribute 를 줘서 대체할 수 있다.

redux-persist 설정

도입 근거 및 고민

recoil 을 사용할 때 새로고침하면 상태가 날아가는 현상이 있었다. 서버에서 hydration 하기전 html 을 만들어서 클라이언트로 보냈는데 클라이언트가 서버로부터 데이터를 다시 받지 않고 새로고침만 해서 상태값이 날라간 것이었다.

이때 이 문제를 해결하기 위해 recoil-persist 를 사용했는데 redux 역시 redux-persist 가 있어서 필요할 것이라 생각했다.

새로고침하거나 앱을 종료해도 store 가 리셋되는 것을 방지하기 위해 사용했다.

redux-persist 를 사용시 localstorage 와 sessionstorage 에 상태를 저장하게 해준다.

  • localStorage 에 store 를 저장하고 싶을때
    import storage from 'redux-persist/lib/storage'
  • sessionStorage 에 store 를 저장하고 싶을때
    import storageSession from 'redux-persist/lib/storage/session'

이번엔 localStorage 에 store 를 저장하고자 storage 를 import 했다.

설치

npm i redux-persist

@types/redux-persist 는 deprecated 되었으니 타입스크립트를 사용중이라 해서 따로 설치할 필요 없다.

사용법

// store.ts
const persistConfig = {
  key: "root",
  storage,
  whitelsit: ["cards"], // 오직 cards 만 persisted 되도록 설정
}

const persistedReducer = persistReducer(persistConfig, cardsSlice.reducer)

export const store = configureStore({
  reducer: {
    cards: persistedReducer,
  },
})

persistConfig 와 persistReducer 를 생성한다.

//main.tsx

import React from "react"
import ReactDOM from "react-dom/client"
import { Provider } from "react-redux"
import { RouterProvider, createBrowserRouter } from "react-router-dom"
import { persistStore } from "redux-persist"
import { PersistGate } from "redux-persist/integration/react"
import { ThemeProvider } from "styled-components"

import Root from "./Root"
import { store } from "./app/store"
import { Form } from "./features/form/Form"
import { Preview } from "./features/preview/Preview"
import { Result } from "./features/result/Result"
import { RouterInfo } from "./routes/RouterInfo.js"
import GlobalStyle from "./style/GlobalStyle.js"
import defaultTheme from "./style/defaultTheme.js"
import "./index.css"

const router = createBrowserRouter(RouterInfo)

// eslint-disable-next-line prefer-const
let persistor = persistStore(store)

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <ThemeProvider theme={defaultTheme}>
      <Provider store={store}>
        <GlobalStyle />
        <PersistGate loading={null} persistor={persistor}>
          <RouterProvider router={router} />
        </PersistGate>
      </Provider>
    </ThemeProvider>
  </React.StrictMode>,
)

그리고 App 혹은 RotuerProvider 를 PersistGate 로 감싸자.
이는 지속되고 있는 상태가 검색되고 redux 에 저장될 때까지 app 의 UI 가 렌더링되는 것을 지연시켜준다.

마주한 오류 Redux Toolkit - A non-serializable value was detected in an action

이 에러는 action 에 직렬화가 불가능한 값을 전달했기 때문에 발생하는 에러다.

정확하게는 Redux Toolkit 에서 자동으로 생성해주는 action 객체가 action 생성자 함수 형태라서 이런 오류가 발생할 수 있다. type 의 인자로 string 이 전달되어야 하는데 함수가 전달되어서 오류가 발생한 것이다.

이때 직렬화란 redux 에서 값을 주고받을 때 object 형태의 값을 string 형태로 변환하는 것 (JSON.stringify) 을 말한다.

역직렬화는 직렬화와 반대로 문자열 혀앹의 객체를 다시 object 형태로 되돌리는 과정을 말한다. (JSON.parse)

redux 가 state, action 에 직렬화가 불가능한 값을 전달할 수 없기에 이 에러가 발생한 것이다.

이때 미들웨어를 추가해서 직렬화 체크를 해제하는 식으로 해결할 수도 있다.

에러가 사라졌다.

Drag & Drop - react-beautiful-dnd 설정

도입 근거 및 고민

질문 카드별로 드래그 & 드랍 기능을 구현해야 했고 적절한 라이브러리인 react-beautifyl-dnd 를 찾게 되어 도입하게 되었다.

설치

npm i react-beautiful-dnd 
npm install --save @types/react-beautiful-dnd

typescript 도 같이 사용중이니 타입도 같이 설치했다.

react hook form 설정

도입 근거 및 고민

한번 써보면 그 다음부터는 안쓸 이유가 없는 라이브러리다. 그저 input 과 submit, onChange 를 사용하면 입력한 값이 바뀔 때마다 불필요한 재렌더링이 발생할 수 있다.

React Hook Form 은 비제어 컴포넌트를 사용하는 라이브러리이며 이와 동시에 제어 컴포넌트인 MUI 를 사용하기 위해 React Hook Form 의 Controller 를 사용하고자 한다. Controller 컴포넌트를 사용하여 MUI 컴포넌트를 매핑시, react-hook-form 이 해당 필드의 상태를 추적하고 필요한 경우에만 업데이트하는 장점을 이용할 수 있다.

React Hook Form -> 이전에 정리했던 내용을 참고해보자.

Controller 와 useController 사이에 무엇을 사용할지 고민해밨는데 useController 는 Controller 와 같은 props, methods 를 공유하고 재사용 가능한 Controlled input 을 생성할 수 있어서 useController 를 사용하였다.

설치

npm i react-hook-form

세부사항

Controller

import Select from "react-select"
import { useForm, Controller } from "react-hook-form"
import Input from "@material-ui/core/Input"


const App = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      firstName: "",
      select: {},
    },
  })
  const onSubmit = (data) => console.log(data)


  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        render={({ field }) => <Input {...field} />}
      />
      <Controller
        name="select"
        control={control}
        render={({ field }) => (
          <Select
            {...field}
            options={[
              { value: "chocolate", label: "Chocolate" },
              { value: "strawberry", label: "Strawberry" },
              { value: "vanilla", label: "Vanilla" },
            ]}
          />
        )}
      />
      <input type="submit" />
    </form>
  )
}

useController

import { TextField } from "@material-ui/core";
import { useController, useForm } from "react-hook-form";


function Input({ control, name }) {
  const {
    field,
    fieldState: { invalid, isTouched, isDirty },
    formState: { touchedFields, dirtyFields }
  } = useController({
    name,
    control,
    rules: { required: true },
  });


  return (
    <TextField 
      onChange={field.onChange} // send value to hook form 
      onBlur={field.onBlur} // notify when input is touched/blur
      value={field.value} // input value
      name={field.name} // send down the input name
      inputRef={field.ref} // send input ref, so we can focus on input when error appear
    />
  );
}

마주한 오류

한글이 깨지는 현상

한글 조합이 안된다.

근데? register 를 하나만 사용할때는 문제가 없는데 2개를 사용시 둘다 한글 조합이 깨져서 영어처럼 분리된 채로 입력이 되었다.

원인을 찾다보니 react hook form 의 register 은 input 혹은 select 요소노드를 등록할 수 있게 해주고 React Hook Form 의 규칙에 따라 유효성 검사를 실시한다.

이것을 위해 string 인 name 을 인자로 전달해야 하는데 위에선 같은 파일 내에서 둘다 id 즉, 동일한 name 을 전달하고 있어서 register 가 제대로 name 을 못찾은 것 같다.

각각 register 에 다른 name 을 전달하니 문제가 해결되었다.

레퍼런스

eslint 를 이용해서 import 순서를 자동으로 바꿔보자! - eamon3481
Eslint & Prettier 설정 방법 (feat. VS Code)
JS프로젝트를 TS로 리팩토링하기
[React] 리액트 라우터 - RouterProvider와 CreateBrowserRouter
React Router v6.4 이상에서 Router 다루기(RouterProvider, createBrowserRouter, Route)
React Router 적용
Redux-persist 란?
redux persist npm
RTK non-serializable value 오류 해결 방법
Redux Toolkit - A non-serializable value was detected in an action ...
react-beautiful-dnd 사용 방법
react-beautiful-dnd npm
[React Hook Form] mui와 같이 사용하기 | Controller, useController
React Hook Form docs
react-beautiful-dnd docs 사용법 영상

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글