Index

Intro

HOF&로컬 스토리지를 활용한 기능 만들기

(예시 코드)

import { useQuery, gql } from "@apollo/client";
import { MouseEvent } from "react";
import {
  IBoard,
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

export default function StaticRoutedPage() {
  const { data } = useQuery<Pick<IQuery, "fetchBoards">, IQueryFetchBoardsArgs>(
    FETCH_BOARDS
  );

  const onClickBasket = (basket: IBoard) => () => {
    console.log(basket);

    // 1. 기존 장바구니 가져오기
    const baskets: Pick<IBoard, "contents" | "title" | "_id" | "writer">[] =
      JSON.parse(localStorage.getItem("baskets") || "[]");

    // 2. 이미 담겼는지 확인하기
    const temp = baskets.filter((el) => el._id === basket._id);
    if (temp.length === 1) {
      alert("이미 담으신 물품입니다!!!");
      return;
    }

    // 3. 해당 장바구니에 담기
    baskets.push(basket);
    localStorage.setItem("baskets", JSON.stringify(baskets));
  };

  return (
    <>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.writer}</span>
          <span style={{ margin: "10px" }}>{el.title}</span>
          <button onClick={onClickBasket(el)}>장바구니담기</button>
        </div>
      ))}
    </>
  );
}

코드를 구현하며 쿠키, 로컬 스토리지, 세션 스토리지 중 어떤 것을 이용해도 좋다.
각 브라우저 저장소의 특성을 고려해 기획 의도에 적합한 저장소를 선택할 것

비회원 장바구니 기능 구현
1) fetchBoards를 이용해 데이터 불러온 후 장바구니 담기 버튼 만들기
2) 온클릭시 HoF 이용해 해당하는 내용 데이터 객체 받기
3) 해당 데이터 객체를 로컬 스토리지에 넣을 수 있는 형태로 가공

데이터를 그대로 로컬 스토리지에 넣으면 제대로 입력되지 않음([Object object])와 같은 식으로 들어가기 때문에 JSON.stringify 등을 이용해 꼭 string으로 변환해 넣어야 한다.

또 제외하고 싶은 데이터가 있을 때 delete를 사용하기보다는 rest 파라미터를 이용해 나머지 데이터를 추출하는 방식으로 가공하는 것이 좋다.

const onClickBasket = (el: IBoard) => () => {

	// 로컬스토리지에 baskets가 이미 있다면 해당 데이터를 불러온다.
  const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");

	// 장바구니에 추가할 게시글 데이터(el)에서 필요 없는 내용을 제거한다.
  const { __typename, ...newEl } = el;

	// baskets에 새로운 데이터를 push한다.
  baskets.push(newEl);
};

이후 가공한 데이터를 로컬 스토리지에 넣어준 후

localStorage.setItem("baskets", JSON.stringify(baskets));

baskets에 동일한 게시글이 있는 경우에는 alert를 띄우고 함수 실행 종료!

const temp = baskets.filter((basketEl: IBoard) => basketEl._id === el._id);
if (temp.length === 1) {
  alert("이미 담으신 물품입니다!!!");
  return;
}

완성된 형태

// 비회원 장바구니에 클릭한 게시글을 넣어주는 함수
const onClickBasket = (el: IBoard) => () => {
  const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
	const temp = baskets.filter((basketEl: IBoard) => basketEl._id === el._id);
	if (temp.length === 1) {
	  alert("이미 담으신 물품입니다!!!");
	  return;
	}

  const { __typename, ...newEl } = el;
  baskets.push(newEl);
  localStorage.setItem("baskets", JSON.stringify(baskets));
};

장바구니 보기 기능 구현(비회원)

방금 넣은 내용을 불러와 장바구니에서 볼 수 있게 하려면..

1) 먼저 로컬스토리지에 있는 데이터를 불러 state에 저장

import { useState } from "react";
import { IBoard } from "../../src/commons/types/generated/types";

export default function BasketLoggedInPage() {
  const [basketItems, setBasketItems] = useState([]);

  const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
  setBasketItems(baskets);

  return (
    <div>
      <h1>나만의 장바구니(비회원전용!!)</h1>
      {basketItems.map((el: IBoard) => (
        <div key={el._id}>
          <span>{el.writer}</span> | 
					<span>{el.title}</span>
        </div>
      ))}
    </div>
  );
}

2) 위와 같이 짜면 프리렌더링 과정에서 로컬스토리지가 없기 때문에 오류 발생 -> useEffect

import { useEffect, useState } from "react";
import { IBoard } from "../../src/commons/types/generated/types";

export default function BasketLoggedInPage() {
  const [basketItems, setBasketItems] = useState([]);

  useEffect(() => {
    const baskets = JSON.parse(localStorage.getItem("baskets") || "[]");
    setBasketItems(baskets);
  }, []);

  return (
    <div>
      <h1>나만의 장바구니(비회원전용!!)</h1>
      {basketItems.map((el: IBoard) => (
        <div key={el._id}>
          <span>{el.writer}</span> | 
					<span>{el.title}</span>
        </div>
      ))}
    </div>
  );
}

3) 완료

폼 라이브러리(react-hook-form)

state, onchange함수 등을 일일히 만들지 않아도 된다?
react-form, redux-form, react-hook-form, formik?

yarn add react-hook-form

가장 사용하기 쉽고 성능이 좋은 react-hook-form

직접 onChange 만들어 setState에 저장하고 바인딩하는 방법은 state가 변화할 때마다 렌더링-> 비효율적이고 느리다.

react-hook-form은 input의 값을 실시간으로 state에 반영하는 것이 아닌 등록함수가 실행될 때 한 번에 처리하기 때문에 불필요한 렌더링이 제거되고 한번에 바꿔 렌더링 -> 빠르고 효율적

위와 같은 방식을 비제어 컴포넌트라고 한다. 리액트 훅 폼 : 비제어 컴포넌트

주의) default가 비제어 컴포넌트 방식이 ㄹ뿐이며 mode에 onChange를 사용해 제어 컴포넌트로 사용할 수도 있다.

📍 비제어 컴포넌트 vs 제어 컴포넌트

  • 비제어 컴포넌트 : 바닐라 자바스크립트 처럼 sumit함수를 실행할 때 ref 로 input값을 한번에 변경 // 단순한 폼(단점) // 리렌더링 미발생(장점)
  • 제어 컴포넌트 : 사용자의 입력을 기반으로 state를 실시간으로 관리(setState 사용). 쉽게 말해 입력이 될 때마다 state값이 실시간으로 변경 // 복잡한 폼 //리렌더링 발생(단점)

한치의 오차도 용납할 수 없는 중요한 데이터를 저장하고 있다면 제어 컴포넌트를 이용하는게 좋다.
하지만 그런게 아니라면 비제어 컴포넌트를 이용해서 성능을 높여주는게 더 좋음.

React-Hook-Form 사용

const ReactHookForm = ()=>{
	// react-hook-form 에서 useForm을 제공합니다.
	const {register , handleSubmit} = useForm()

	// 등록하기 함수 -> handleSubmit이 조종해주는 함수 입니다.
	const onClickSubmit = (data)=>{
		console.log(data)
	}

	return(
		<form onSubmit={handleSubmit(onClickSubmit)}>
			<input type="text" {...register("writer")}/>
			<input type="text" {...register("title")}/>
			<input type="text" {...register("contents")}/>
			<button type="reset"> 등록하기 </button>
		</form>
	)
}
export default ReactHookForm

register : state를 등록하는 데 필요한 모든 기능
handleSubmit: register에 적힌 state를 등록해주는 함수
form: 실제 html에 있는 Input들을 묶어줌

react-hook-form의 구조
form 태그는 Input에 적힌 내용을 전송해주는 기능이 있고 react-hook-form에서는 이 기능을 이용, button태그의 type에 reset을 주게 되면 클릭시에 폼 안에 있는 인풋값을 초기화함

submit을 주면 form태그에 바인딩된 submit 함수 실행, 버튼타입의 기본은 submit으로 폼 안에서 사용하면 명시하지 않아도 submit의 기능을 하게 된다. 만일 form태그 내에서 form과 상관없는 버튼을 만들어야 하면 type을 button으로 주어야 함.

form 내부의 button type
1) reset : form 내부의 input 값 모두 삭제
2) submit : form 내부의 input 값 백엔드로 보내짐(기본값)
3) button : 나만의 버튼을 만들고 싶을 때

주의사항) form 태그 내의 button 타입이 submit일 경우, 버튼에 다른 함수를 바인딩할 시 예상치 못한 오류가 발생할 수 있다. form 태그의 submit 함수와 바인딩 된 함수를 모두 처리하기 때문

검증 라이브러리(yup)

yup 공식 문서 https://www.npmjs.com/package/yup

검증 라이브러리는 보통 form과 함께 사용한다. 이 때 리액트 훅 폼의(Schema Validation-https://react-hook-form.com/get-started#SchemaValidation)을 참고하고 사용하면 된다.

yarn add @hookform/resolvers yup
yarn add yup

참고) resolver에서는 yup 이외에도 다른 검증 라이브러리를 사용할 수 있다.

yup 사용

import * as yup from 'yup'
import {useForm} from 'react-hook-form'
import {yupResolver} from '@hookform/resolvers/yup'


// yup 에러메세지 생성해주기 -> 제어 컴포넌트 형태로 사용해야 합니다.
const schema = yup.object().shape({
	myWriter : yup.string()
								.email('이메일 형식이 적합하지 않습니다.')
								.required('필수 입력값입니다.')
	myPassword : yup.string()
									.min(4,'비밀번호는 최소 4자리 이상입니다.')
									.max(15,'비밀번호는 최대15자리 입니다.')
									.required('필수 입력값 입니다.') 
})

const ReactHookForm = ()=>{
	//formState에서 에러메세지들을 받아오게 됩니다.
	const {register , handleSubmit, formState} = useForm({
		// schema는 위에서 만들어 둔 schema입니다.
		resolver : yupResolver(schema),
		mode : "onChange"
	})

	// 등록하기 함수 -> handleSubmit이 조종해주는 함수 입니다.
	const onClickSubmit = (data)=>{
		console.log(data)
	}

	return(
		<form onSubmit={handleSubmit(onClickSubmit)}>
			이메일 : <input type="text" {...register("myEmail")}/>
				{ /* 우리가 생성한 yup의 에러메세지는 항상 errors에 담기는데 이 에러는 있을때도 있고 없을 때도 있기 때문에 옵셔널 체이닝을 붙여야 합니다. */}
				<div> {formState.errors.myEmail?.message}</div>
			비밀번호 : <input type="text" {...register("myPassword")}/>
				<div> {formState.errors.myPassword?.message}</div>
			<button styled={{ backgroundColor: formState.isValid ? "yellow" : "" }}> 등록하기 </button>
		</form>
	)
}
export default ReactHookForm

에러가 있을 수도, 없을 수도 있기 때문에 옵셔널 체이닝을 통해 조건부 렌더링을 걸어 주어야 한다. 또 최종적으로 에러가 있는지 없는지 확인 후 버튼을 활성화할 때는 formState의 isValid를 사용하면 된다.

참고) yup 에 정규 표현식 추가하기
yup.string().matches(/ 원하는 정규표현식! /)

참고) schema? 구조를 의미한다. 보통 하나의 구조를 스키마라고 하는데 만들어둔 하나의 yup을 스키마라고 하겠다.

Destructing (비구조화 할당/ 구조분해 할당)

배열의 구조분해 할당

const classmates = ["철수","영희","훈이"]

배열의 원소를 각각 child1, 2, 3 에 할당할 때 사용하던 방법

const child1 = classmates[0] 이런 식으로 각각 해주었다.

//베열의 구조분해 할당
const [child1, child2, child3] = classmates

선언부에 대괄호 사용, 객체의 비구조화 할당과 다르게 변수명은 무관하며 할당부에는 배열의 이름을 넣는다. 원소 할당은 인덱스 순서대로 들어간다.

useState역시 구조분해 할당이었다. const [state, useState] = useState("")

실행 후 리턴값 : 배열, 이 역시 const bbb = useState("")의 리턴값이 배열이므로 const state = bbb[0], const setCounter = bbb[1]로 사용할 수 있다.

객체의 구조분해 할당

const child = {
	name : “철수”,
	age : 58,
	school : “개발 공장”
}

위의 객체에서 name, age, school 이라는 변수를 만들고 싶다면
const name = child.name 이런 식으로 하나씩 생성했었다.(배열처럼)

그런데 변수가 많아질수록 힘들다. 이런 번거로움을 덜어주는 것이 "비구조화 할당"(변수를 한번에 모두 선언하고 할당)

//객체의 비구조화 할당
const {name, age, school} = child

const {data, loading} = useQuery(FETCH_BOARDS) 역시 객체 구조분해 할당이었다.

useQuery 실행 후 리턴값이 객체이므로.

const aaa = useQuery(FETCH_BOARDS)로 받아와 aaa.data, aaa.loading 이런 식으로 사용할 수 있었던 것! 그러나 이는 비효율적

Rest Parameter

특정 객체에서 지우고 싶은 데이터가 있을 때! delete를 통해 원본을 건드리는 것은 바람직하지 못하다. (에러 발생을 유도할 수 있다.)

const {money, hobby, ...rest}=child

이렇게 적으면 rest부분에는 머니, 하비 제외 모든 것이 들어감.

참고로 반드시 rest라고 적을 필요는 없다.

Custom Hooks

useState, useContext, useEffect 등 다양한 리액트 내장 후크는 모두 함수였고 사용할 때 함수를 호출하듯 사용했다. 커스텀 훅이란 개발자가 스스로 커스텀한 훅을 의미한다.

그런데 훅이 함수라면 커스텀 훅 역시 함수인데 일반 함수와 어떤 차이점이 있을까? 비슷하면 그냥 함수를 쓰면 되는데 왜 사용할까?

내부에서 useState, useEffect와 같은 훅을 사용하게 되면 custom hook이라 한다.

custom hook을 사용하게 되면 함수 네이밍에 use를 사용해야 한다.

use를 쓰지 않으면 실행은 되나 의도와 다르게 작동하거나 훅의 에러 로그를 볼 수 없게 되고 에러 핸들링이 어려워진다.(https://ko.reactjs.org/docs/hooks-custom.html)

custom Hooks로 useAuth 만들어 보기

HOC로 만든 withAuth는 사실 클래스 컴포넌트에서 사용했던 방식이었다. 함수형 컴포넌트에서는 어떻게 ? custom hooks

useAuth

// src/components/commons/hooks/useAuth.tsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'

function useAuth(){
	const router = useRouter()
	// useEffect 훅스를 사용하고 있기 때문에 커스텀 훅스입니다.
	useEffect(()=>{
		if(!localStorage.getItem("accessToken"){
			alert("로그인 후 이용 가능합니다!")
			void router.push("/23-03-login-check")
		}
	},[])
}

custom hook은 컴포넌트가 아닌 함수이기 때문에 return 부분이 JSX가 아님!

// custom-hooks-use-auth 폴더의 index.tsx
// 이 페이지는 useAuth를 실행할 페이지입니다.

import { useAuth } from "...파일 공유"

export default function CustomUseAuthPage() {
	useAuth() 
	
  return <div>프로필 페이지 입니다.</div> 
}

이 페이지를 실행하면 상단의 useAuth()가 먼저 실행된다. 그럼 토큰을 확인 후 존재하면 프로필 페이지가 정상 작동, 그렇지 않다면 로그인 후에 이용하도록 경고창 띄울 것.

custom hooks -> useMoveToPage

매번 router를 불러오고 코드를 직접 작성해 페이지를 이동하면 비효율적 -> custom hook으로 분리, 관리

아래는 커스텀 훅으로 분리한 라우팅을 사용할 컴포넌트

// custom-hooks-use-move-to-page 폴더의 index.tsx
import { useMoveToPage } from "커스텀 훅의 파일 ";

export default function CustomHooksUseMoveToPage() {
	// 커스텀 훅으로 분리한 라우터 사용 -> 리턴을 객체로했기때문에 객체로 받아오는 것 입니다.
  const { onClickMoveToPage } = useMoveToPage();

  return (
    <>
      <button onClick={onClickMoveToPage("/boards")}>게시판으로 이동</button>
      <button onClick={onClickMoveToPage("/markets")}>마켓으로 이동</button>
      <button onClick={onClickMoveToPage("/mypage")}>마이페이지로 이동</button>
    </>
  );
}

본격적으로 커스텀 라우터 만들기

useMoveToPage()

// src/components/commons/hooks/useMoveToPage.tsx

import { useRouter } from "next/router";
import { useRecoilState } from "recoil";
import { visitedPageState } from "../../../commons/store";

export function useMoveToPage() {
  const router = useRouter();
  const [visitedPage, setVisitedPage] = useRecoilState(visitedPageState);

  const onClickMoveToPage = (path: string) => () => {
    setVisitedPage(path);
    void router.push(path);
  };

  return {
    visitedPage,
    onClickMoveToPage,
  };
}

위 코드를 보면 함수의 인자로 라우팅할 주소를 넘겨주면 해당 페이지로 라우팅됨. 다녀간 페이지들은 RecoilState에 넣어 두고 필요할 때 사용하면 됨. 리턴은 하나만 해줄 수 있으므로 객체로 묶어 리턴!

타입스크립트 Generic

any vs unknown

any : 어떠한 타입이 입력되어도 전부 허용하는 타입, 요소에 any를 부여하면 사실상 타입스크립트가 아닌 자바스크립트를 사용하는 것이나 마찬가지

unknown: 개발자에게 주의를 주는 용도, 연산에 오류가 발생할 수 있음을 경고

타입 정리
1) 문자

// 문자 타입 지정 예시
export function getString(arg: string): string{
	return arg;
}
const result1 = getString("철수");
console.log(result);

2) 숫자

// 숫자 타입 지정 예시
export function getNumber(arg: number): number {
	return answer;
};
const result2 = getNumber(123);
console.log(result2);

3) any

// any 타입 지정 예시
export function getAny(arg: any): any {
	return arg;
}
const result31 = getAny("철수");
const result32 = getAny(8);
const result33 = getAny(true);

any는 인자에 어떤 타입이 들어가도 전부 any라고 반환

Generic 타입
인자에 들어오는 타입을 그대로 사용

// Generic 사용 예제
export function getGeneric<MyType>(arg: MyType): MyType {
  return arg;
}
const aaa: string = "철수";
const bbb: number = 8;
const ccc: boolean = true;
const result41 = getGeneric(aaa);
const result42 = getGeneric(bbb);
const result43 = getGeneric(ccc);




arg가 string 타입으로 들어갈 때는 string 타입이, number 타입으로 들어갈 때는 number타입이, boolean 타입으로 들어가면 boolean 타입이 된다.

Generic 응용

여러 개의 인자가 들어가는 함수에 Generic 적용하고 싶은 경우,
MyType 부분은 함수처럼 원하는 이름 지정 가능(너무 길어서 실무에서는 T,U,V등 간단한 이름 사용)

Generic 타입 활용(HOC)

useQuery, useMutation처럼 내가 만든 기능을 타인에게 제공하는 경우 해당 기능에 들어오는 값의 타입을 예상할 수 없다. 이 때 제너릭을 사용하면 들어오는 값의 타입에 따라 반환값이나 컴포넌트 타입이 결정되도록 할 수 있다.

HOC에 적용하려면

1) Closure - 기초

함수를 리턴하는 함수, HOF에 type 지정

2) Closure - 기초(Any)
인자에 항상 고정된 타입만 들어오지 않으면? any를 쓸 수 있으나 안정성 저하

3) Closure - 기초(Generic)

인자에 어떤 타입이 들어가도 해당 타입을 반환

4) Closure - 기초(Generic) 화살표 함수
함수 선언식으로 작성된 HOF를 화살표 함수로 바꿔도 동일하게 적용 가능

5) Closure - 기초(Generic) HOC
화살표 함수로 작성된 제너릭 예시를 HOC로 변경

6) withAuth에 Generic 적용

컴포넌트 타입은 리액트에서 제공하는 ComponentType 을 이용한다. 그리고 props에는 반드시 객체가 들어가야 스프레드 연산자를 사용할 수 있기 때문에, extends를 이요해 P라는 Generic 타입이 객체라는 사실을 명시해야 한다.

profile
Strive for greatness

0개의 댓글