노마드코더 ReactJS 마스터클래스 5

딩쓰·2023년 4월 12일
0
post-thumbnail

6. STATE MANAGEMENT (6.0 ~ 6.19)

#6.0 ~ 6.2 (2023.04.12 수)

이번 섹션에서는 Recoil을 이용해서 state management하는 법을 배워보자.

6.0 다크/라이트 모드 구현하기 1 (props 만으로)

Recoile은 페이스북에서 만든 React Js에서 사용할 수 있는 state management임. 미니멀하고 간단해서 이해하기 쉽고 쓰기 쉬움.

우선 state management가 왜 필요한지 이해하기 위해 state management를 사용하지 않고 코인 어플리케이션에 다크모드/라이트모드가 되는 toggle 버튼을 만들어 볼거임.

  • src/index.tsx 파일에 있던 <ThemeProvider/>App.tsx 파일로 옮겨줌.

  • 옮기고 난뒤, theme.ts로 가서 원래있던 theme을 darkTheme으로 바꾸고 반대로 lightTheme도 만들어줌.
  • App.tsx에 다시가서 isDark state를 만들어 누를때마다 false <-> true가 되도록 함.
  • isDark가 true일때 다크모드로, false일때 라이트모드로 변하는 토글 버튼을 만듦.

결과 화면 ↓

ThemeProvider를 index에서 App으로 옮긴 이유는 state를 사용하려고 옮긴것!

6.1 다크/라이트 모드 구현하기 2 (props 만으로)

토클 버튼을 홈페이지의 제목옆의 위치로 옮기고 싶어서 App.tsx에 있는<button/>Coins.tsx에 옮겨줌.

  • App에서 버튼을 지우면 toggleDark function을 Coins로 보내야 함.

  • 마찬가지로 Chart screen에서도 dark mode로 선택되어 있어서 어플리케이션의 상태가 다크모드인지 아닌지 알아야 하기때문에 isDark state에 접근할 수 있어야함.

일단 state management를 사용하지 않고 props 만으로 제목옆의 토글버튼을 클릭해서 다크모드를 끄고 킬 수 있게 바꿔보자.

  • toggleDark function을 Coins.tsx에서 사용하기 위해
    App -> Router -> Coins 순서대로 props를 내려줌.
  • 타입스크립트를 사용하기 때문에 각각 interface를 생성해서 props를 정의해줌.

  • interface에 function을 정의할때 위와 같이 마우스를 대면 함수의 type이 나오는데 이걸 복사해서 붙여넣으면 됨.
  • () => void; : argument를 받지 않고 아무것도 return하지 않는다는 의미

이번엔 chart에게 다크모드인지 아닌지 isDark state를 props로 내려줘야함.

  • App -> Router -> coin -> chart 순서로 props를 내려줌.

  • chart 컴포넌트에서 받아온 props로 isDark가 true면 다크모드 false면 라이트모드가 되게 구현함

  • chart까지 라이트모드가 구현된 것을 볼 수 있음
//props 내려준 과정
App (isDark, modifierFn)

App-> Router -> Coins (modifier)
App-> Router -> coin -> Chart (isDark)

위에서 작업한 props를 내리는 과정은 너무 길고 효율적이지 않았음.
💡 state들을 따로 모아놓고 원하면 가져올 수 있는 state management가 필요한 이유임!

6.2 Recoil로 다크/라이트 모드 구현하기 1

//props 내려준 과정
App (isDark, modifierFn)

App-> Router -> Coins (modifier)
App-> Router -> coin -> Chart (isDark)
  • 위에선 App과 Chart 컴포넌트만 isDark value(true or false)가 필요한 상황인데 지금까지 props로 내려주고 있었음. 이제 value를 하나의 비눗방울 안에 두고 사용할건데 Recoil에서 이 비눗방울을 ⭐️Atom 이라고 부름!

리코일은 이해하기 쉬운 state management임. 공식홈페이지의 영상을 참고하기!

function App() {
  // const [isDark, setIsDark] = useState(false);
  // const toggleDark = () => setIsDark((current) => !current);

  return (
    <>
      <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
        {/* <button onClick={toggleDark}>Toggle Dark Mode</button> */}
        <GlobalStyle></GlobalStyle>
        <Router></Router>
        <ReactQueryDevtools initialIsOpen={true} /> {/* 2 */}
      </ThemeProvider>
    </>
  );
}

export default App;
  • 리코일을 시작하기 전에 지금까지한 isDark와 modifier function props들을 다 삭제해주자.

Recoil 세팅하기

npm install recoil
1. 위의 명령어로 리코일을 설치함.

//index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import { RecoilRoot } from "recoil"; // 1. import 해오기
import App from "./App";

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <div>
    <RecoilRoot>  {/* 1 */}
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </RecoilRoot>
  </div>
);
  1. src/index.tsx로 이동해서 <RecoilRoot/> 감싸주자. QueryClientProvider과 비슷함.

시작하는 방법은 여기까지가 다임. 매우 간단함!

//src/atoms.ts
import { atom } from "recoil";

export const isDarkAtom = atom({
  key: "isDark",
  default: true,
});
  • atom을 사용하기 위해 src/atoms.ts 파일을 생성함. 여기서는 리코일의 atom function을 사용해야함.
  • 이 funtion은 두 가지를 요구하는데 첫 번째는 key로 유일한 이름이여야 하고, 두번째는 기본값임.

이제 atom이 필요한 App과 Chart 컴포넌트에 atom의 value랑 연결해보자.

  • App.tsx로 가서 useRecoilValue() 훅에 isDarkAtom을 넣으면 isDark를 사용할 수 있음.
    -> dafault값이 false이기 때문에 타입스크립트가 isDark가 boolean이라는 것을 인지함.

  • 똑같은 방법으로 Chart에도 atom을 연결해줌.

위와 같은 순서대로 하면 라이트/다크모드가 잘 구현됨.
✔️결과적으로 App과 Chart 컴포넌트에만 Atom의 value와 연결해서 사용한것임!

#6.3 ~ 6.6 (2023.04.13 목)

6.3 Recoil로 다크/라이트 모드 구현하기 2

함수를 사용하기 위해서 atom의 value를 어떻게 수정하는지 배워보자.

  • 일단 홈페이지의 타이틀 옆에 button을 만들어줌.

  • atom의 value를 감지하기 위해서는 useRecoilValue라는 hook을 쓰지만 atom의 value를 수정하기 위해서는 useSetRecoilState라는 hook을 씀
  • useSetRecoilStat는 function을 가져오는데 이 function이 value를 수정할거임
    -> ⭐️React의 setState(수정함수) 와 같은 방식으로 작동함!
  • isDarkAtom을 꼭 import 해야함.
  • button의 onClick으로 연결해서 setDarkAtom 함수에 이전 value를 가져와서 클릭할 때마다 값이 반대로 바뀌게 해줌.

  • 위와 같이 코드축약도 가능함.

리코일로 다크모드/라이트모드 구현결과 ↓

6.5 Recoil 세팅

리코일을 사용한 todo-list를 만들기 위해 세팅해주자!

npx create-react-app myApp --template typescript
npm i --save-dev @types/styled-components
npm i styled-components
npm i recoil
기존의 코인 어플리케이션 폴더에서 만들어도 상관없지만 새로 만들고 싶다면 위와 같이 세팅해주면 됨. (나는 위의 명령어들로 폴더를 새로 만들고 코인 어플리케이션의 코드를 붙여넣어줌.)


위와 같은 환경에서 시작함.(리액트,타입스크립트,리코일)


  • App.tsx,index.tsx,theme,styled.d.ts 파일만 남겨줌.
  • App.tsx에 기존의 GlobalStyle을 붙여넣기 해줌.

이러면 세팅은 끝!
이제 recoil의 다른 함수와 기능들에 대해 배워보자. atoms 말고도, 배우면 좋은 다른 기능들이 많음.

//App.tsx
import { createGlobalStyle } from "styled-components";
import ToDoList from "./ToDoList";

const GlobalStyle = createGlobalStyle`
//코드생략
`;

function App() {
  return (
    <>
      <GlobalStyle />
      <ToDoList />
    </>
  );
}

export default App;
  • TodoList 컴포넌트를 만들어서 App.tsx에 렌더링 시켜줌.

  • TodoList 컴포넌트에서 form에 input과 button을 만들고 form에는 onSubmit함수, input에는 onChage함수를 만들어서 연결 해줌.
  • event: React.FormEvent<HTMLInputElement> 이 속성은 타입스크립트를 사용할 때만 필요함.
  • const { currentTarget: { value },} = event; : 이 문법은 es6문법으로 const value = event.currentTarget.value 와 같은 뜻.
  • console.log로 input의 value가 잘 들어가는 걸 확인함.

6.6 React Hook Form

이번 강의에서는 react-hook-form에 대해 배워보자.
react-hook-form은 모든 걸 쉽게 만들어줘서 리액트에서 form으로 작업하기에 가장 좋은 방법임. 다양한 form 관리 방법과 라이브러리들 중에 최고라고 할 수 있음. 큰 form들과 form validation이 많을 때도 유용함.


react-hook-form은 위의 모든 걸 한 줄의 코드로 대체 가능!

실제 앱을 빌드를 한다고 가정해볼때, user가 계정을 생성하거나 회원가입을 하면 당연히 수많은 input들을 가지게 될건데(이메일,이름,성,비밀번호,비밀번호 확인창 등등) react-hook-form을 사용하지 않는다면 아래와 같이 form에 수많은 state 들을 가져야 할거임.

게다가 큰 규모의 앱을 만든다면 form validation이 필요할 거임.

  • 위와 같이 toDo만 검증한다고 가정해보면 form을 submit할 때, 검증을 직접 해주고 에러도 직접 표시해줘야 함.
  • submit하면 에러가 뜨고 onChange 이벤트가 작동하면 에러가 사라지게 만듦.

하나의 항목도 이 정도로 해야 하는데 이름,성,아이디,이메일,비밀번호 등등 input창이 여러 개면 직접 하는것이 매우 번거로울 거임.

리액트 훅 폼 세팅

npm install react-hook-form

위의 명령어로 react-hook-form을 설치해주자.

//ToDoList.tsx
// import React, { useState } from "react";
import { useForm } from "react-hook-form";

// function ToDoList() {
//   const [toDo, setToDo] = useState("");
//   const [toDoError, setToDoError] = useState("");

//   const onChange = (event: React.FormEvent<HTMLInputElement>) => {
//     const {
//       currentTarget: { value },
//     } = event;
//     setToDoError("");
//     setToDo(value);
//   };

//   const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
//     event?.preventDefault();
//     if (toDo.length < 10) {
//       return setToDoError("To do should be longer");
//     }
//     console.log("submit");
//   };

//   return (
//     <div>
//       <form onSubmit={onSubmit}>
//         <input onChange={onChange} value={toDo} placeholder="Write a to do" />
//         <button>Add</button>
//         {toDoError !== "" ? toDoError : null}
//       </form>
//     </div>
//   );
//
function ToDoList() {
  const { register, watch } = useForm();

  return (
    <div>
      <form>
        <input placeholder="Write a to do" />
        <button>Add</button>
      </form>
    </div>
  );
}

export default ToDoList;
  • 위와 같이 useForm hook을 import 해주자.
  • 원래 있던 코드는 주석처리 하고, input의 속성은 지우고 비교해 볼 거임.
  • useForm은 많은 것들을 제공하는데 그중 register하는 함수를 사용하면 onChange 이벤트 핸들러와 input의 props가 필요없음. (useState도 필요 없어짐)

  • console.log(register("todo"))를 하면 onChange, onBlur이벤트 등을 return하고 있고, register 함수에는 문자열을 보내줘야 name에 할당됨.
    (onBlur란 input의 바깥쪽 포커스를 클릭하는 것)
//ToDoList.tsx
function ToDoList() {
  const { register, watch } = useForm();

  return (
    <div>
      <form>
        <input {...register("todo")} placeholder="Write a to do" />
        <button>Add</button>
      </form>
    </div>
  );
}

export default ToDoList;
  • register 함수가 object를 return하니 위와 같이 input props에 spread 문법으로 넣어주자.
  • useForm을 사용한 단 한줄의 코드가 onChange 이벤트와 value, useState를 모두 대체함.

이제 작동하는지 알아보기 위해 useForm 함수의 watch를 사용해 보자.
watch는 form의 입력값들의 변화를 추적할 수 있게 해주는 함수임

  • console.log(watch())를 하면 toDo를 키값으로, input의 적은 입력값을 value를 가지고 있는 객체를 리턴함.

  • input을 여러개 만들어서 콘솔로 확인해 보면 하나의 객체안에 form이 만들어지고 모든게 잘 연결된 것을 확인 가능.
  • register 함수 인자에는 띄어쓰기는 안되고 소문자,대문자는 상관없음.
  • const { register, watch } = useForm(); 단 한줄의 코드로 원래의 코드를 대체함!
    결론: 새로운 input을 만들 때마다 register 함수만 실행해주면 되서 시간을 많이 절약됨!

#6.7 ~ 6.9 (2023.04.14 금)

6.7 Form Validation


위의 form의 onSubmit 이벤트를 리액트 훅 폼으로 대체해 보자.

  • useForm에 handleSubmit이란 함수를 받아와야 함.
  • handleSubmit은 validation 뿐만 아니라 이벤트를 preventDefault 하는 것도 담당함.
  • 데이터가 유효하지 않다면, useForm이 에러를 보여주는 onValid 라는 함수를 만들어 줌.
    -> onValid는 react-hook-form이 모든 validation을 마쳤을 때만 호출될 거임
  • form onSubmit 이벤트를 등록해서 handleSubmit을 호출하게 하고 handle Submit은 2개의 인자를 받음.
  • handleSubmit(): 하나는 데이터가 유효할 때 호출하는 함수(onValid)고, 다른 하나는 데이터가 유효하지 않을 때 호출되는 함수(onInvalid)임.
    -> onValid는 필수임.
  • onValid 함수를 인자로 데이터를 받고 매개변수 타입을 any라고 해주고, data를 console.log 해줌.

정리: 유저가 submit을 하면 handleSubmit은 해야하는 모든 validation이나, 다른 일들을 전부 끝마친 후에 데이터가 유효할 때만 onValid 함수를 호출함.

위와 같이 하면 onSubmit 이벤트가 대체됨. 이제 required 속성을 넣어보자

//ToDoList.tsx
function ToDoList() {
  const { register, handleSubmit } = useForm();
  const onValid = (data: any) => {
    console.log(data);
  };

  return (
    <div>
      <form onSubmit={handleSubmit(onValid)}>
        <input
          {...register("email", { required: true, minLength: 10 })}
          placeholder="email"
        />
        <input
          {...register("firstName", { required: true })}
          placeholder="firstName"
        />
        <input
          {...register("lastName", { required: true })}
          placeholder="lastName"
        />
        <input
          {...register("userName", { required: true, minLength: 10 })}
          placeholder="username"
        />
        <input
          {...register("password", { required: true, minLength: 5 })}
          placeholder="password"
        />
        <input
          {...register("password1", { required: true, minLength: 5 })}
          placeholder="password1"
        />
        <button>Add</button>
      </form>
    </div>
  );
}
  • 원래는 HTML태그에 required 속성을 넣어서 (ex:<input required/>) 작동하게 할 수 있지만, 유저가 지원하지 않는 브라우저나 모바일에서 본다거나 혹은 브라우저의 소스코드를 수정할 수 있는 위험이 있음.
    -> HTML에 의지하는 대신, 위와 같이 자바스크립트에서 validation을 하기위해 register 함수 안에다가, {required: true}를 넣어줌.

  • {required: true}로 인해 입력하지 않은 input으로 커서를 자동으로 옮겨줌. 이것이 리액트 훅 폼의 장점!
    -> 이걸 직접 스스로 만든다면 어떤 항목이 비어있는지 알아야 하고, 비어있는 첫항목이 어딘지 알아내서 사용자를 거기로 옮겨야 할거임.

  • minLength: 5if(toDo.length < 10) 조건문을 대신해줌

이제 useForm에 formState라는 프로퍼티를 하나 더 받아보자!


이 프로퍼티의 에러를 console에 찍어보면 에러의 type을 알려줌.

  • 위와 같이 에러에 메시지도 보낼수 있음.

  • ValidatonRule은 값과 메시지를 함께 가지고 있는 객체인데, 문자열이나 숫자 아니면 다른 것도 보낼 수 있음.

6.8 Form Errors

이제 에러를 유저들에게 보일 수 있게 해보자!

function ToDoList() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

useForm에 있던 formState를 위와 같이 바꿔주자.

  • 모든 input요소들에게 required: "메시지" 를 작성해줌.
  • 또다른 validation 방법으로는 정규식을 사용하는 방법이 있는데(RegExp) 이정규식을 이메일 입력창에 쓰기위해 pattern을 넘기고 value에 정규표현식을 붙여넣어줌.(사이트 참고)
  • span을 만들어서 input창 밑에 에러메시지가 뜨게 만들어 줌.
  • 타입스크립트 에러가 나는건 {errors?.email?.message as string} 로 고쳐줌.

정규표현식
^ 문장의 시작

  • 하나 또는 하나이상
/^[A-Za-z0-9._%+-]+@naver.com$/
/^[A-Za-z0-9._%+-]+@naver.com/g
  • A-Z: A부터 Z까지의 대문자
  • a-z: a부터 z까지의 소문자
  • 0-9._%+-: 숫자와 부호들 전부


모든 input밑에 span을 붙여 넣어주면 이제 모든 input창 밑에 에러메시지가 뜨게 하는거 완성!

지금은 data를 any라고 했는데, 타입스크립트에게 any라고 알려주는 건 좋지 않기 때문에 타입스크립트에게 form이 모양을 interface로 알려줘보자.

  • interface를 생성해주고 어떤걸 입력 받아야 하는지 적어준 다음, IForm을 useForm에 제네릭으로 넣어주자.
  • defaultValues옵션: 미리 기본값을 설정할 수 있음

6.9 Custom Validation

어떤 경우에는 서버에서 문제가 발생해서 에러를 발생시켜야 할 경우도 있어서 에러를 어떻게 발생시킬 수 있는지를 알아보자.
그리고 지금 까지 배운 검사 방법(minLength, required, 정규식) 이외의 원하는 규칙에 따라 만들 수 있는 validation 방법을 배워보자.

예를 들어 유저가 username을 적는 도중에 API에 요청을 보내서 사용자명이 이미 존재하는지 확인한다음 존재한다면, 사용자에게 즉시 화면에 알려줄 수 있음.
이런건 react-hook-form으로 개발자가 만든 규칙에 따라 검사하는 것을 만들 수 있기 때문에 가능함.

password와 password1(일치 확인용)이 같지 않은 경우에 에러


  • data의 타입을 any에서 IForm으로 바꿔 줌.
  • useForm에 특정한 에러를 발생시키는 함수인 setError를 만듦.
  • if(조건)으로 password와 password1이 같지 않다면, setError를 해줌.(타입스크립트 덕분에 set Error를 만들기만 해도 항목들의 이름을 볼 수 있음.)
  • {shouldFocus: true}로 password1에 오류가 있다면 form의 커ㅓ서가 자동으로 password1에 옮겨지게 만듦.

전체 form에 대한 에러

사용자가 form을 제출했는데 서버가 다운되서 접속이 끊겼을때 같은 상황에 사용 가능함.

  • setError에 extraError를 넣어줌.
  • extraError: 특정한 항목에 해당하는 에러가 아니라, 전체 form에 해당되는 에러
  • setError는 발생하는 분제에 따라 추가적으로 에러를 설정할 수 있게 도와주고 form에서 유저가 고른 input항목에 강제로 focus되는 기능이 있어서 유용함!

원하는 대로 만드는 유효성 검사

이름이 nico거나 이름에 nico를 포함하고 있는 유저 가입을 막는 상황을 만들어 보자.

  • validate 옵션은 함수를 값으로 가지고 인자로 항목에 현재 쓰여지고 있는 값을 받음.
  • nico가 포함된 이름이면 화면에 에러가 렌더링 되고 아니면 true로 validation이 통과되게 만듦.
  • validate 옵션은 하나의 함수 또는 여러 함수가 있는 객체가 될 수 있음. -> input에 여러 개의 검사가 필요할 수 있기 때문
  • validate 함수를 async로 비동기로 만들어서 서버에다가 확인하고 응답을 받을 수도 있음.
profile
Frontend Developer

0개의 댓글