Understanding useReducer와 Context API

🥔감자로그🍟·2024년 7월 25일
0

강의 속도가 점점 빨라지기 시작했다. 하루에도 투두리스트를 5번씩 갈아엎는 것 같다. 오늘은 뭘 배웠지...useCallback이랑 useMemo, React.Memo, useReducer, Context API를 배웠다. 그 중 useReducercontext API를 복습해보라고 과제를 내주셨다.

원래는 오늘 12시까지 제출 마감이었는데 용기있고 멋진 분께서 강사님과 엄청난 협상을 한 끝에 제출기한이 내일 아침 9시까지로 늘어났다🙃햅삐...

괜차나 괜차나 닝닝닝닝닝


🧐개념

Hook

Hook이란, React 16.8 버전부터 추가된 기능이다. 함수 컴포넌트에서 React 상태와 생명주기 기능을 연동할 수 있게 해주는 함수

useState

useReducer와 비슷한 useState를 먼저 다시 복습해보자.

React에서 상태 관리를 위해 사용하는 기본적인 Hook이다. 현재 상태의 값을 제공하고, 상태 값을 업데이트 하는 함수를 반환한다.

const [state, setState] = useState(초기값);

컴포넌트의 현재 값은 state 변수에 들어있다. setState는 상태를 갱신하기 위해 사용하는 함수이다. setState함수가 호출되면 상태값이 업데이트되고, 컴포넌트가 리렌더링 된다.

import { useState } from 'react';
import './App.css';

function App() {
  const [count, setCount] = useState(0);
  console.log("rerendered");
  
  return (
    <>
      <h3>useState를 알아보자</h3>
      <h1>Count : {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>증가</button>
    </>
  );
}

export default App;

위 예시 코드에서 상태 변수 count와, 함수 setCount를 선언했다.

코드를 실행하고 버튼을 눌러보면, count 값이 버튼 클릭 때마다 하나씩 증가하는 것을 확인할 수 있다. 콘솔 로그를 확인해보면, count 값이 하나 증가할 때마다 리렌더링 되는 것도 확인할 수 있다.

정리
useState를 사용하여 컴포넌트 내에서 상태를 쉽게 관리할 수 있고, 상태가 변경될 때마다 컴포넌트를 리렌더링하여 상태 값이 반영된 UI를 제공할 수 있다. 이를 활용하여 React 어플리케이션을 동적이고 반응적으로 구현할 수 있다!

🚫주의
useState를 사용하면 state가 변경될 때마다 다시 렌더링되는데, 불필요한 렌더링을 일어나게 한다. 또, 한 번 리렌더링 될 때마다 콘솔 로그가 두 번씩 찍히는게 이는 React.StrictMode 때문이다. 오류가 아니라 정상적인 현상이니 놀라지 말기

useReducer

useState와 같은 상태를 관리하는 훅이다. 복잡한 상태 관리 로직을 처리하기 위해 사용한다.

const [state, dispatch] = useReducer(reducer, initialArg, init);

기본 형태는 위와 같다.

  • reducer: 상태 업데이트 방식을 지정하는 함수
  • initailArg: 상태 초기 값
  • init: state 초기 값을 지정할 수 있는 초기화 함수

현재의 상태 값과, 상태를 업데이트하고 리렌더링을 유발하는 함수를 반환한다.

reducer 함수

useReducer 선언 시 첫 번째 인자로 reducer 함수를 넘겨준다. reducer 함수는 state와 action, 두 개의 매개 변수를 가진다.

const reducer = (state, action) => {}
  • state: 현재 상태 값
  • action: dispatch 함수로 받을 액션 또는 다음 상태 값

새로운 상태 값을 반환한다.

dispatch 함수

useReducerdispatch 함수를 반환한다. action 객체를 매개변수로 받아서 reducer 함수로 전달한다.

  • dispatch: 액션 객체를 받아 상태 업데이트를 트리거하는 함수
  • action: 수행할 작업 또는 다음 상태 값을 지정하는 객체

위에서 useState를 사용하여 구현한 카운터를 useReducer을 사용한 형태로 변경해보았다.

감소와 초기화 기능을 추가했는데, 이처럼 상태의 업데이트에 관련된 별도의 기능을 reducer 함수에 집중시켜 한번에 처리할 수 있다.

import { useReducer } from 'react';
import './App.css';

//리듀서 함수를 정의
const reducer = (state: number, action: { type: string; payload?: number }) => {
  if (action.type === 'increment' && action.payload)
    return state + action.payload;
  else if(action.type === 'decrement' && action.payload)
    return state - action.payload;
  else if(action.type === "reset")
    return 0;
  else return state;
};
function App() {
  
  //useReducer를 사용하여 상태와 디스패치를 초기화한다
  const [state, dispatch] = useReducer(reducer, 0);

  return (
    <>
      <h3>useState를 알아보자</h3>
      <h1>Count : {state}</h1>
      <button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
        증가
      </button>
    </>
  );
}

export default App;
  • useReducer을 사용해서 상태 값 statedispatch를 초기화한다. 초기 상태 값은 0이다.
  • 버튼을 클릭하면 dispatch 함수가 호출되고, action 객체 {type: "increment", payload : 1}reducer로 전달된다. 상태 값이 payload에 설정한 만큼 증가한다.

💡실습

useReducer을 이해하기 위해 다른 기능을 만들어 보았다. 숫자는 이제 질리도록 올리고 내려봐서 boolean을 활용한 기능을 구현해 보았다.

import { useReducer } from 'react';
import './App.css';

const reducer = (state: boolean, action: { type: string }) => {
  if (action.type === 'toggle') return !state;
  else return state;
};
function App() {
  const [isOn, dispatch] = useReducer(reducer, false);

  return (
    <>
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <h3>On, Off 토글해보기</h3>
        <div
          style={{
            width: '50px',
            height: '50px',
            backgroundColor: isOn ? 'yellow' : 'black',
            marginBottom: '30px',
            borderRadius: '100px',
            boxShadow: isOn ? '0 0 40px' : 'none',
          }}
        ></div>
        <button onClick={() => dispatch({ type: 'toggle' })}>불켜기</button>
      </div>
    </>
  );
}

export default App;

버튼을 누르면 on, off 되는 간단한 코드이다.

useReducer을 사용하여 isOn과 dispatch를 초기화 하였다. 초기 값은 false이다.

근데 useReducer의 장점은 여러 기능을 reducer내에서 집중 처리할 수 있다는 것인데, 단순 onoff만 할거면 useReducer을 사용하는 의미가 없다. 기능을 조금 더 추가해보자

import { useReducer } from 'react';
import './App.css';

type Action =
  | { type: 'toggle' }
  | { type: 'setColor'; color: string }
  | { type: 'setShape'; shape: string };

type State = {
  isOn: boolean;
  color: string;
  shape: string;
};

const init: State = {
  isOn: false,
  color: 'black',
  shape: '100px',
};
const reducer = (state: State, action: Action) => {
  if (action.type === 'toggle')
    return {
      ...state,
      isOn: !state.isOn,
      color: state.isOn ? 'black' : 'yellow',
      shape: state.isOn ? '100px' : '100px',
    };
  else if (action.type == 'setColor') return { ...state, color: action.color };
  else if (action.type == 'setShape') return { ...state, shape: action.shape };
  else return state;
};
function App() {
  const [light, dispatch] = useReducer(reducer, init);

  return (
    <>
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <h3>내마음대로 만드는 전구(?)</h3>
        <div
          style={{
            width: '50px',
            height: '50px',
            backgroundColor: light.color,
            marginBottom: '30px',
            borderRadius: light.shape,
            boxShadow: light.isOn ? '0 0 40px' : 'none',
          }}
        ></div>
        <button onClick={() => dispatch({ type: 'toggle' })}>불켜기</button>
        <button onClick={() => dispatch({ type: 'setColor', color: 'red' })}>
          색바꾸기
        </button>
        <button onClick={() => dispatch({ type: 'setShape', shape: '10px' })}>
          모양바꾸기
        </button>
      </div>
    </>
  );
}

export default App;
  • isOn, color, shape를 포함한 상태를 정의한 State 타입을 정의하였고, 초기 상태 init을 정의하였다.
  • reducer 함수에서 액션을 처리하여 상태를 업데이트 한다.
    • toggle은 isOn을 반전시킨다.
    • setColor은 color을 받아 색상을 변경한다.
    • setShape는 shape를 받아서 모양을 변경한다.
  • useReducer을 사용하여 상태와 디스패치 함수를 초기화하고, 각 버튼에 액션을 연결하여 버튼 클릭 시 리듀서에서 해당 액션을 처리한다.
  • 상태 관리 로직을 reducer에서 집중적으로 처리하여, 버튼 클릭 이벤트에 따라 상태를 업데이트하여 UI를 변경한다!

정리
useReducer을 사용하여 복잡한 상태 로직을 단순화하고, 상태 변경 로직을 한 곳에서 관리할 수 있다. 가독성과 유지보수성이 향상된다!

독립적이고 단순한 로직 처리에 적합한 useState, 복잡하고 의존적인 상태 관리에 적합한 useReducer

Context API

props를 사용하지 않고 필요한 데이터를 쉽게 공유할 수 있게 해준다.

이렇게 컴포넌트가 연결되어 있을 때, 데이터를 요하는 컴포넌트가 가장 하위에 있어도 최상위 컴포넌트부터 순차적으로 값을 전달 해주어야 한다. Props Drilling, 프롭스 드릴링이라고 한다.

해당 값을 중간 컴포넌트가 사용하지 않아도, 전달을 위해 값을 받고, 전달해주어야 한다.
중간 컴포넌트에서 불필요한 값을 받는다는 점, 계층 간에서 이름이 변경되면 값을 추적하고 업데이트하기 번거로운 등 단점이 존재한다.

이를 해결하기 위한 방법 중 하나가 Context API이다. Context를 생성하고 값을 제공하는 컴포넌트를 만들고, 필요한 컴포넌트에서 useContext를 사용하여 해당 값을 직접 접근하여 사용한다. 중간 컴포넌트를 거치지 않는다!

Context API를 알아보자(Bad Practice)

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

export const counterContext = createContext<{
  count: number;
  setCount: React.Dispatch<React.SetStateAction<number>>;
}>({ count: 0, setCount: () => {} });

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <counterContext.Provider value={{ count, setCount }}>
        <Page />
      </counterContext.Provider>
    </>
  );
}
import { useContext } from "react";
import { counterContext } from "../../App";

const DisplayCounter = () => {
  const { count } = useContext(counterContext);
  console.log("display counter");

  return (
    <>
      <h1>Counter: {count}</h1>
    </>
  );
};
export default DisplayCounter;

Context API를 사용하는 코드이다.

  • createContext를 사용하여 context를 생성한다.
  • 값을 전달해주기 위해 Provider로 대상 컴포넌트를 감싼다.
  • Providervalue에 전달할 데이터를 넣는다.(위 코드에서는 count와 setCount를 전달한다)

위 코드를 실행하면 < <counterContext.Provider value={{ count, setCount }}>...</counterContext.Provider>사이에 있는 모든 컴포넌트가 리렌더링 된다. 데이터를 전달받지 않는 컴포넌트까지 리렌더링 되기 때문에 불필요한 처리(React.Memo() 등을 사용한 메모이제이션)가 필요하다. 이를 방지하기 위해 context 컴포넌트를 분리하고 children을 사용한다.

정석은 context 컴포넌트를 만드는 것

import { createContext, useState } from "react";

export const CounterContext = createContext<{
  count: number;
  setCount: React.Dispatch<React.SetStateAction<number>>;
}>({ count: 0, setCount: () => {} });

export const CounterContextProvider = ({
  children,
}: {
  children: React.ReactNode;
}) => {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
};

children을 사용하여 값을 전달해주고 있다. 이렇게 코드를 작성해주면, context를 사용하지 않는 컴포넌트는 직접 메모이제이션 하지 않아도 리렌더링에서 제외된다.


회?고

점점 속도가 빨라지고 매일 3,4시간씩 통학하는것도 힘들지만 그래도 수업을 들어보니 제대로 배우는것 같아서 재밌는것 같다. 배우면 배울수록 내가 그동안 어떤 💩들을 만들어왔는지 깊게 반성하게 되는 것 같다. 내일은 useEffect와 zustand를 배울거라고 하셨다. 졸작 프로젝트 하던 중에 상태관리 때문에 심하게 스트레스 받고 인생이 힘들고 우울하고 암울하고 탈모 온 것 같고 마음이 너무 아팠었는데 그 🔥HOT🔥한 Zustand를 배울 수 있게 되어 너무 기대된다.


🐶억울 이슈 발생

글 이쁘게 쓰고 싶어서 스타일 적용해봤더니 미리보기에서 잘보이길래 열심히 썼는데 출간하면 안보인다 너무 억울하다 진짜 세상 이럴수가 업음

profile
멋진 회오리 감자가 되는 그날까지 https://monicx.tistory.com/

0개의 댓글