본문 바로가기

Frontend Study

[유데미x스나이퍼팩토리] Next.js 3기 - 사전직무교육 5일차

고차 컴포넌트

컴포넌트를 반환하는 컴포넌트

React Memoization

리액트는 상태가 변경 될 때 마다 컴포넌트를 리렌더링함 → 컴포넌트가 많거나 복잡한 로직을 가진 함수가 있으면 성능 저하의 우려가 있음

⇒ 로직의 계산값을 기억해 동일한 계산을 하지 않도록 해주는 기법

React Memo

React.memo로 감싼 컴포넌트와 하위 컴포넌트는 메모이제이션 됨

Memoization이 해제되는 조건

  1. 컴포넌트 내의 상태가 변경 되는 경우
  2. 컴포넌트로 전달되는 props가 변경되는 경우
  3. props로 함수가 전달되는 경우 → 컴포넌트가 렌더링 될 때 마다 함수가 재정의 되기 때문

memoization이 해제되면 하위 컴포넌트 역시 렌더링 됨

// A.tsx
import { useState } from "react";
import React from "react";

export default React.memo(function A() {
  const [cl, setCl] = useState(true);

  console.log("A rendered");
  return (
    <>
      <div className="flex flex-col gap-4">
        <div
          className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
          onClick={() => setCl(!cl)}
        >
          D
        </div>
        <E />
      </div>
    </>
  );
});

//App.tsx
import { useState } from "react";
import A from "./components/A";

export default function App() {
  const [cl, setCl] = useState(true);
  return (
    <>
      <div className="item-middle flex-col gap-4">
        <div
          className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
          onClick={() => setCl(!cl)}
        >
          App
        </div>
        <A />
      </div>
    </>
  );
}
// APP 컴포넌트를 클릭하면 App만 렌더링
// A 컴포넌트는 변경 사항이 없기 때문에, 렌더링 되지 않음 

// A 컴포넌트를 클릭시 변동사항이 생기기 때문에, A 컴포넌트만 렌더링 됨

useCallback

useCallback은 함수 메모이제이션에 쓰이는 hook

useCallback(콜백함수, 의존성 배열) → 의존성 배열의 값이 바뀌지 않으면 실행되지 않음

import { useCallback, useState } from "react";
import A from "./components/A";

export default function App() {
  const [cl, setCl] = useState(true);
  const log = useCallback(() => console.log(cl), [cl]);
  //cl의 값이 바뀌지 않으면 log()가 실행되지 않음
	log();
  return (
    <>
      <div className="item-middle flex-col gap-4">
        <div
          className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
          onClick={() => setCl(!cl)}
        >
          App
        </div>
        <A />
      </div>
    </>
  );

useMemo

useCallback가 함수를 메모이제이션한다면 useMemo는 값을 메모이제이션 한다

import { useMemo, useState } from "react";
import A from "./components/A";

export default function App() {
  const [cl, setCl] = useState(true);
  const cal = (cl: boolean) => {
    let sum = 0;
    let op = cl ? 1 : -1;
    for (let i = 0; i < 100000; i++) {
      sum++;
    }
    return sum * op;
  };
  //cl의 값이 바뀌지 않으면 cal()함수가 실행되지 않고, 계산 결과가 변하지 않음
  const result = useMemo(() => cal(cl), [cl]);
  return (
    <>
      <div className="item-middle flex-col gap-4">
        <div
          className="w-[100px] h-[50px] bg-orange-500 text-white flex items-center justify-center"
          onClick={() => setCl(!cl)}
        >
          {result}
        </div>
        <A />
      </div>
    </>
  );
}

useReducer

useReducer(reducer, 초기값)

reducer는 함수 형태 ⇒ 초기값 state와, state를 변경 할 수 있는 요소인 action을 매개변수로 가짐

→ useReducer은 reducer 함수의 반환값을 사용

Redux와 사용 방법 비슷

import { useReducer, useState } from "react";

const reducer = (state: number, action: string) => {
  switch (action) {
    case "INCREASE":
      return state + 1;
    case "DECREASE":
      return state - 1;
    case "RESET":
      return 0;
    default:
      return state;
  }
};

function App() {
	//cnt의 초기값은 useReducer의 매개변수로 전달한 초기값
	//이후 Dispatch(setCnt) 함수로 변경된 값이 cnt에 저장됨
  const [cnt, setCnt] = useReducer(reducer, 0);

  return (
    <>
      <div>현재 값 : {cnt}</div>
      <button onClick={() => setCnt("INCREASE")}>증가</button>
      <button onClick={() => setCnt("DECREASE")}>감소</button>
      <button onClick={() => setCnt("RESET")}>초기화</button>
    </>
  );
}
export default App;

reducer를 다른 파일에서 export 하여 여러 컴포넌트에 사용 가능

//countReducer.ts
export const reducer = (state: number, action: string) => {
  switch (action) {
    case "INCREASE":
      return state + 1;
    case "DECREASE":
      return state - 1;
    case "RESET":
      return 0;
    default:
      return state;
  }
};

//App.tsx
import { useReducer, useState } from "react";
import { reducer } from "./reducer/countReducer";

function App() {
  const [cnt, setCnt] = useReducer(reducer, 0);

  return (
    <>
      <div>현재 값 : {cnt}</div>
      <button onClick={() => setCnt("INCREASE")}>증가</button>
      <button onClick={() => setCnt("DECREASE")}>감소</button>
      <button onClick={() => setCnt("RESET")}>초기화</button>
    </>
  );
}
export default App;

useContext

context API를 통해 전역적으로 상태 관리를 할 수 있게 도와주는 hook

context를 사용하고자 하는 컴포넌트들의 최상위 컴포넌트에 Context 컴포넌트와 Provider 속성을 추가해 줘야 함

Context 컴포넌트의 value props에 사용하고자 하는 값 또는 메서드들을 전달

import { createContext, useState } from "react";
import Page from "./components/Page";

//Context 컴포넌트
export const CounterContext = createContext<{
  count: number;
  setCount: React.Dispatch<React.SetStateAction<number>>;
} | null>(null);
function App() {
  const [count, setCount] = useState(0);
  return (
    <>
	    //Provider 속성과 value를 추가한 Context 컴포넌트
      <CounterContext.Provider value={{ count, setCount }}>
        <Page />
      </CounterContext.Provider>
    </>
  );
}
export default App;

useContext를 이용해 설정한 context를 사용

import { useContext } from "react";
import { CounterContext } from "../App";

function Page() {
	//useContext를 사용해 CounterContext에 접근
  const { count, setCount} = useContext(CounterContext)!;
  return (
    <>
      <h1>{count}</h1>
      <button onClick={() => setCount((count) => count + 1)}>증가</button>;
    </>
  );
}
export default Page;

Provider로 감싸져 있는 한 컴포넌트 계층 구조에 상관 없이 Context(변수) 사용 가능

👏 Non-null Assertion Operator ⇒ 구문 뒤에 **!**를 붙여 개발자 차원에서 null이 아님을 단언

Provider는 따로 컴포넌트로 빼서 사용 하기도 함

//CounterProvider.tsx
import { createContext, useState } from "react";

export const CounterContext = createContext<{
  count: number;
  setCount: React.Dispatch<React.SetStateAction<number>>;
} | null>(null);
function CounterProvider({ children }: { children: React.ReactNode }) {
  const [count, setCount] = useState(0);
  return (
    <>
      <CounterContext.Provider value={{ count, setCount }}>
        {children}
      </CounterContext.Provider>
    </>
  );
}
export default CounterProvider;

//App.tsx
import Page from "./components/Page";
import CounterProvider from "./context/CounterProvider";

function App() {
  return (
    <>
	    //Provider를 하나의 컴포넌트로 만들어 사용
      <CounterProvider>
        <Page />
      </CounterProvider>
    </>
  );
}
export default App;

zustand

떠오르는 전역 상태 관리 툴 → store에 상태와 set 함수를 작성하고 컴포넌트에서 불러와 사용 가능

import { create } from "zustand";
interface CounterStore {
  count: number;
  inc: () => void;
}
export const useCountStore = create<CounterStore>((setCount) => ({
  count: 0,
  inc: () =>
    setCount((state) => ({
      count: state.count + 1,
    })),
}));

 

객체의 Key에 변수 할당

const keyName = "age"
const obj = {
	name: 'john',
	[keyName]: 20
}

//name: john
//age: 20

코드에 적용하여 코드 간소화 가능

import { useState } from "react";

export default function Basic() {
	//객체 형식의 formState를 지정
  const [formState, setFormState] = useState({
    name: "",
    email: "",
    pw: "",
    pwConfirm: "",
  });

  const onChangeFormState = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormState((formState) => ({
      ...formState,
      //name:value 형식의 객체 요소를 추가
      [e.target.name]: e.target.value,
    }));
  };

  return (
    <>
      <form action="">
	      // 이 input이 실행되면 formState는 
		    // name:(input 내용)이라는 속성을 가짐
        <input
          className="border"
          name="name"
          type="text"
          onChange={onChangeFormState}
          value={formState.name}
        ></input>
        <input
          className="border"
          name="email"
          type="text"
          onChange={onChangeFormState}
          value={formState.email}
        ></input>
        <input
          className="border"
          name="pw"
          type="text"
          onChange={onChangeFormState}
          value={formState.pw}
        ></input>
        <input
          className="border"
          name="pwConfirm"
          type="text"
          onChange={onChangeFormState}
          value={formState.pwConfirm}
        ></input>
      </form>
    </>
  );
}

Custom Hook

다양한 기능을 하는 커스텀 함수들을 만들어 React Hook처럼 사용

// useInput.tsx
import { useState } from "react";

type useInputReturn = [
  string,
  (e: React.ChangeEvent<HTMLInputElement>) => void
];
export const useInput = (initialValue: string): useInputReturn => {
  const [value, setValue] = useState(initialValue);
  const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  //useInput은 입력한 value와 입력 함수 onChangeHandler를 리턴
  return [value, onChangeHandler];
};

//App.tsx
import { useInput } from "../utils/utils";

function App() {
  const [name, onChange] = useInput("");
  return (
    <>
      <form action="">
        <input
          className="border"
          value={name}
          type="text"
          onChange={onChange}
        ></input>
      </form>
    </>
  );
}
export default App;

데이터 통신

NEXT.JS는 axios보다는 Fetch API를 더 선호하는 편

REST API

REprentational State Transfer API

  • GET
  • PUT
  • PATCH
  • DELETE
  • OPTION

FETCH API

Fetch API를 이용해 서버로부터 데이터를 받아 옴

Fetch는 비동기로 동작하기 때문에, 브라우저는 Fetch 내부 로직을 기다려주지 않음

→ 데이터를 받아오기 전에 브라우저가 렌더링되어 버리면 undefined 에러가 날 수 있음

비동기 처리

  1. Promise
    • pending : 데이터를 받기 위해 대기중인 상태
    • fulfilled : 데이터를 성공적으로 받아 Promise 객체로 받은 상태
      • then() 메서드를 통해 데이터 사용
      • response 객체를 이용하여 데이터에 접근 가능
      • 단, fetch는 promise 객체만을 반환하기 때문에, useState를 이용해 response 객체로부터 받은 data를 할당해 주어야 함
    • rejected : 데이터 전송이 거절된 상태
import { useEffect, useState } from "react";

function Fetch() {
  const [data, setData] = useState([]);
  useEffect(() => {
    fetch("<https://jsonplaceholder.typicode.com/todos>", {
      method: "GET",
    })
      .then((res) => res.json())
      .then((data) => setData(data));
  }, []);
  return (
    <>
      {data.map((el) => (
{JSON.stringify(el)}
      ))}
      ;
    
  );
}
export default Fetch;
  1. async / await

비동기 작업을 실행할 함수를 async 키워드로 감싼 뒤, 실행할 동작들 앞에 await 키워드를 붙여준다

→ await를 만나면 awiat의 실행이 끝날 때 까지 다른 동작을 실행하지 않음

import { useEffect, useState } from "react";

function Fetch() {
  const [data, setData] = useState([]);
  const fetchData = async () => {
    const response = await fetch("<https://jsonplaceholder.typicode.com/todos>", {
      method: "GET",
    });
    const data = await response.json();
    setData(data);
  };
  useEffect(() => {
    fetchData();
  }, []);
  return (
    <>
      {data.map((el) => (
{JSON.stringify(el)}
      ))}
      ;
    
  );
}
export default Fetch;

결과 타입은 제네릭으로 설정

에러 핸들링

try-catch문을 이용해 에러 핸들링

  • fetch 단계에서 발생하는 에러는 catch에서 처리
  • 이외의 에러는 throw new Error를 통해 에러 정의
  • 모든 과정이 끝난다면 finally를 통해 fetch 종료
import { useEffect, useState } from "react";

function Fetch() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState(false);
  const fetchData = async () => {
    setIsLoading(true);
    //try문에서 fetch 시작
    try {
      const response = await fetch(
        "<https://jsonplaceholder.typicode.com/todos>",
        {
          method: "GET",
        }
      );
      const data = await response.json();
      
      //에러가 발생할 경우 에러 정의
      if (!response.ok) throw new Error("서버 통신 오류");
      setData(data);
    } catch (error) {
	    //에러 출력
      setIsError(true);
      console.log(error);
    } finally {
	    //모든 동작 종료
      setIsLoading(false);
    }
  };
  useEffect(() => {
    fetchData();
  }, []);
  return (
    <>
      {isError ? (

Error

      ) : isLoading ? (

Loading

      ) : (
        data.map((el) =>
{JSON.stringify(el)}
)
      )}
      ;
    
  );
}
export default Fetch;

라우팅

라우팅은 url 주소를 통해 다른 페이지로 이동하게 해주는 방법

과거와 많이 달라져서, routes나 route 등 복잡한 방법을 사용하지 않고도 쉽게 라우팅을 할 수 있게 되었다

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./css/index.css";
import App from "./App.tsx";
import Basic from "./components/Basic.tsx";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

//라우터 배열 생성
const router = createBrowserRouter([
	// 라우터 배열 내에는 path(url 주소)와 element(이동할 페이지 컴포넌트)가 들어감
  {
    path: "/",
    element: <App />,
  },
  {
    path: "/login",
    element: <Login />,
  },
]);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
	  //RouterProvider을 이용해 라우터 사용
    <RouterProvider router={router}></RouterProvider>
  </StrictMode>
);

App.tsx보다는 view 폴더 내에 HomeView.tsx, LoginView.tsx 등의 파일로 생성

router가 길어지는 것을 대비해 외부 파일로 빼기도 함

Outlet

라우터에서는 Children 대신 Outlet이라는 컴포넌트를 사용해 내용을 변경

//DefaultLayout.tsx
import { Outlet } from "react-router-dom";
import Footer from "../components/Footer";
import Header from "../components/Header";

function DefaultLayout() {
  return (
    <>
      <Header />
      <Outlet />
      <Footer />
    </>
  );
}
export default DefaultLayout;

//router.tsx
import { createBrowserRouter } from "react-router-dom";
import HomeView from "../view./HomeView";
import LoginView from "../view./LoginView";
import DefaultLayout from "../view./DefaultLayout";

const router = createBrowserRouter([
  {
    path: "/",
    element: <DefaultLayout />,
    // children 속성으로 원하는 컴포넌트들을 넘겨줌
    children: [
      {
        path: "/home",
        element: <HomeView />,
      },
      {
        path: "/login",
        element: <LoginView />,
      },
    ],
  },
]);
export default router;

등록되어있지 않은 모든 주소는 *로 처리

import { createBrowserRouter } from "react-router-dom";
import HomeView from "../view./HomeView";
import LoginView from "../view./LoginView";
import DefaultLayout from "../view./DefaultLayout";

const router = createBrowserRouter([
  {
    path: "/",
    element: <DefaultLayout />,
    children: [
      {
        path: "/home",
        element: <HomeView />,
      },
      {
        path: "/login",
        element: <LoginView />,
      },
    ],
  },
  {
    path: "*",
    element: <h1>없는 주소입니다</h1>
  }
]);
export default router;

다른 페이지로 이동하게 만들고 싶으면 Link 태그를 사용하면 됨

//HomeView.tsx
import { Link } from "react-router-dom";

function HomeView() {
  return (
    <>
      <div>LoginView</div>
      <Link to="/login">로그인으로</Link>
    </>
  );
}
export default HomeView;

//LoginView.tsx
import { Link } from "react-router-dom";

function LoginView() {
  return (
    <>
      <div>LoginView</div>
      <Link to="/home">홈으로</Link>
    </>
  );
}
export default LoginView;

useNavigate() 훅 역시 이동 용도로 사용 가능

import { useNavigate } from "react-router-dom";

function LoginView() {
  const navigate = useNavigate();
  return (
    <>
      <div>LoginView</div>
      <button onClick={() => navigate("/home")}>홈으로</button>
    </>
  );
}
export default LoginView;

이외 router용 hooks

  1. useParams
    • url의 다양한 쿼리를 받을 때 사용
  2. useSearchParams
    • url의 검색 쿼리를 받을 때 사용
  3. useLocation
    • 현재 위치를 나타내줌

본 게시글의 예제 코드는 수코딩(https://www.sucoding.kr)을 참고했습니다