React-8 HOOKS (23/03/07)

nazzzo·2023년 3월 7일
0

React HOOKS



1. useCallback & useMemo



함수형 컴포넌트의 이슈에 대해...


// 함수형 컴포넌트
export const Counter = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(0)

  const increment = () => {
    setCount(count + 1);
    console.log(`+`)
  };
  const decrement = () => {
    setCount(count - 1);
    console.log(`-`)
  };

  const view = () => {
    console.log("실행");
    return count;
  };

  return (
    <>
      <h2>{view()}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <h2>{value}</h2>
      <button onClick={()=>{setValue(value+1)}}>+</button>
    </>
  );
};


// 클래스형 컴포넌트
class Counter2 extends React.Component {
  state = { count: 0 };

  increment() {
    this.setState(this.tate.count + 1);
  }
  decrement() {
    this.setState(this.tate.count - 1);
  }
  view() {
    return this.state.count;
  }

  render() {
    return (
      <>
        <h2>{this.view()}</h2>
        <button onClick={() => this.increment()}>+</button>
        <button onClick={() => this.decrement()}>-</button>
      </>
    );
  }
}

클래스형 컴포넌트에서는 render() 함수 내에서 호출한 함수만 실행됩니다

반면 함수형 컴포넌트는 위 예제가 보여주듯이 컴포넌트의 상태가 바뀔 때(리렌더링)마다
그 안의 모든 함수가 의도치 않게 재실행된다는 문제가 있는데요
이로 인해 불필요한 리렌더링을 발생시킬 수 있습니다

리액트는 메모이제이션 기법을 활용해서 이에 대한 몇가지 해결책을 제공하고 있습니다

메모이제이션?
기존에 수행한 연산의 결과값을 메모리에 저장해두고, 동일한 연산이 필요한 경우
연산의 재실행 없이 저장된 값만 재활용하는 프로그래밍 기법입니다



1-1. useMemo


useMemo()는 메모이제이션된 을 반환하는 함수입니다

useCallback(함수 값, [추적할 상태]) => 함수의 리턴값

  • 상태가 바뀌면 함수를 재실행하고, 상태가 바뀌지 않으면 함수를 재실행하지 않고 메모리에 저장된 값을 반환합니다
    (*만약 2번째 인자의 배열이 비워져있다면 최초로 렌더링된 상태를 호출하고
    함수 호출로 인해 상태가 바뀌어도 함수 실행에 대한 결과값을 리렌더링하지 않으니 주의!)

예제1

  const datetime = new Date().toISOString();

  const today = useMemo(()=> {
    return datetime
  }, [count])
  
  return <h2>{datetime}</h2>

상태가 바뀔 때마다 화면에 표시되는 시간이 달라집니다


예제2

import { useState, useMemo } from "react";

export const Memo = () => {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

  // 홀수만 구하기
  const oddNumbers = useMemo(() => {
    return numbers.filter((v) => v % 2 !== 0);
  }, [numbers]);

  const handleClick = () => {
    setNumbers([...numbers, numbers.length + 1]);
  };

  return (
    <div>
      <p>숫자 : {numbers.join(", ")}</p>
      <p>홀수 : {oddNumbers.join(", ")}</p>
      <button onClick={handleClick}>요소 추가</button>
    </div>
  );
};

위 예제에서 oddNumbersnumbers 배열에서 홀수만을 필터링한 값으로,
배열이 업데이트될 때마다 매번 계산되어야 합니다
위와 같이 useMemo()를 사용하면, numbers 배열이 업데이트되었을 때
이전에 계산된 oddNumbers 변수의 값을 재사용할 수 있기 때문에
매번 연산하는 비용을 줄일 수 있습니다



1-2. useCallback, memo


useCallback(함수 값, [추적할 상태]) => 함수 내용

useCallback()은 결과값만 반환하는 useMemo()와 달리, 상태가 변할 때마다 함수의 내용 전체를 반환합니다


리액트 컴포넌트에서 렌더링이 발생하면, 컴포넌트 내부의 모든 함수가 새로 생성됩니다
이는 상태 변경으로 인한 리렌더링에서도 마찬가지인데요

반면 useCallback()은 이전 렌더링에서 생성된 함수를 기억하고, 다음 렌더링에서 같은 함수가 필요한 경우에는 이전에 생성된 함수를 재사용합니다
렌더링이 발생할 때마다 함수를 새로 생성하지 않아도 되기 때문에,
이로 인한 성능상 이점을 가져올 수 있다는 것이 핵심입니다


예제

import { useState } from "react";

export const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setValue(value + 1);
  };

  return (
    <>
      <h2>Count: {count}</h2>
      <ChildComponent increment={increment}></ChildComponent>

      <h3>{value}</h3>
      <button onClick={decrement}>-</button>
    </>
  );
};

const ChildComponent = ({ increment }) => {
  console.log("리렌더링");
  return <button onClick={increment}>+</button>;
};

위 코드를 실행해보면 자식 컴포넌트와 무관한(프롭스를 전달하지 않은) - 버튼을 눌러도
자식 컴포넌트가 함께 리렌더링되는 것을 확인할 수 있습니다


useCallback()memo()를 사용해서 코드를 아래와 같이 수정해보겠습니다


import { useState, useCallback, memo } from "react";

export const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  const decrement = useCallback(() => {
    setValue(value + 1);
  }, [value]);

  return (
    <>
      <h2>Count: {count}</h2>
      <ChildComponent increment={increment}></ChildComponent>

      <h3>{value}</h3>
      <button onClick={decrement}>-</button>
    </>
  );
};

const ChildComponent = memo(({ increment }) => {
  console.log("리렌더링");
  return <button onClick={increment}>+</button>;
})

이제 - 버튼을 눌렀을 때(프로퍼티가 변하지 않았을 때)에는 자식 컴포넌트가 리렌더링되지 않습니다



+)
그런데 사실 소규모 프로젝트에서 useMemouseCallback 사용으로 인한 최적화를 체감하기는 어렵습니다
그리고 useCallbackmemo 함수 사용에도 나름의 비용이 발생하기 때문에, 반드시 더 효율적인 결과를 가져오는 것도 아닙니다
비동기 요청을 쓸 데 없이 재실행하는 경우처럼 리소스 낭비가 심한 게 아니라면 굳이 남용하지 않는 것이 좋다고 하네요

그럼에도 꼭 알아둬야 할 내용임에는 틀림없으니...



2. useContext & useReducer


2-1. useContext


회원가입이 필요한 사이트에서 유저에 관한 상태는 모든 컴포넌트에서 공유할 필요가 있습니다
그런데 최상위 컴포넌트에서 최하위 컴포넌트까지, 상태가 프롭스를 타고 타고 내려가는 것은 너무 비효율적이겠죠
(이런 식의 사용을 Prop Drilling이라고 합니다)

이럴 경우 프로퍼티 전달을 사용하지 않고 모든 컴포넌트에서 관리하는 상태를 전역상태라고 하며,
useContext는 이러한 전역상태를 관리하기 위해 사용합니다


사용 예제

import { useState, createContext, useContext } from "react";

// 전역상태를 생성합니다
const Global = createContext();

const D = () => {
    const text = useContext(Global)
  return <>Hello user : {text}</>;
};
const C = () => {
  return <D></D>;
};
const B = () => {
  return <C></C>;
};
const A = () => {
  return <B></B>;
};

export const Context = () => {
  const [user, setUser] = useState("alpha");

  return (
    <Global.Provider value="alpha">
      <A></A>
    </Global.Provider>
  );
};

↓ 실행 결과

Hello user : alpha



그리고 관리할 데이터가 여럿이라면 객체를 활용하기

import { useState, createContext, useContext } from "react";

// 전역상태를 생성합니다
const Global = createContext();

const D = () => {
  const obj = useContext(Global);
  return (
    <>
      Hello user : {obj.user}
      <button
        onClick={() => {
          obj.setUser("delta");
        }}
      >
        rename
      </button>
    </>
  );
};
const C = () => {
  return <D></D>;
};
const B = () => {
  return <C></C>;
};
const A = () => {
  return <B></B>;
};

export const Context = () => {
  const [user, setUser] = useState("alpha");
  const initialState = {
    user,
    setUser,
  };

  return (
    <Global.Provider value={initialState}>
      <A></A>
    </Global.Provider>
  );
};

결과 rename 버튼을 누르면 모든 컴포넌트에서 유저에 관한 상태가 바뀐 것을 확인할 수 있습니다



2-2. useReducer


reducer 함수는 현재 상태(state) 객체와 행동(action) 객체를 인자로 받아서,
새로운 상태(state) 객체를 반환하는 함수입니다
주로 상태 관리 로직을 따로 꺼내어 쓰기 위해 사용합니다


useReducer(함수값, {초기 상태값}) => [상태값, 상태를 바꾸는 함수]

  • 초기 상태값은 일반적으로 객체 형태로 넣습니다
  • 초기 상태값이 비어 있어도 reducer가 넣어줍니다

const initialState = {}
const reducer = (state) => {
  console.log(state)
}
const [state, dispatch] = useReducer(reducer, initialState)
  • 상태를 바꾸는 함수는 dispatch라는 이름을 사용합니다 (setState, setValue... 와 같은 역할)
  • dispatch()가 실행되면 reducer() 함수를 호출합니다
  • reducer()가 발동되면 인자인 action값에 따라 새로운 상태를 객체 형태로 반환합니다
  • 인자인 action은 객체 형태로, 상태 변경을 일으키는 이벤트에 대한 정보가 담깁니다

action

{
    type: [액션의 종류를 식별할 수 있는 문자열],
    [액션의 실행에 필요한 임의의 데이터],
}



기본 예제

import { useReducer } from "react";

const reducer = (state, action) => {
  console.log(state);

  switch (action) {
    case "increment":
      return { count: state.count + 1, user: state.user };
    case "decrement":
      return { count: state.count - 1, user: state.user };
  }
};

export const CounterReducer = () => {
  const initialState = { count: 0 };
  const [state, dispatch] = useReducer(reducer, initialState);

  const increment = () => {
    dispatch("increment");
  };

  const decrement = () => {
    dispatch("decrement");
  };

  return (
    <>
      <h2>Count : {state.count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
};

위 예제에서 reducer는 분기처리를 해서 코드를 실행하는 역할을 합니다

일반적으로는 아래 예제 형태를 따라 사용합니다


예제2

import { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload, user: state.user };
    case "decrement":
      return { count: state.count - action.payload, user: state.user };
  }
};

export const CounterReducer = () => {
  const initialState = { count: 0 };
  const [state, dispatch] = useReducer(reducer, initialState);

  const increment = () => {
    dispatch({ type: "increment", payload: 1 });
  };

  const decrement = () => {
    dispatch({ type: "decrement", payload: 1 });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const { counter } = e.target;
    console.log(counter.value);

    const action = {
      type: "increment",
      payload: parseInt(counter.value),
    };

    dispatch(action); // 1. type: 어떤 실행을 할 것인지, 2. payload: 바꿀 내용들
  };

  return (
    <>
      <h2>Count : {state.count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <br />
      <form onSubmit={handleSubmit}>
        <input type="text" id="counter" name="counter" />
        <button>+</button>
      </form>
    </>
  );
};

useReducer에 대해서는 다음 포스트에서 더 자세히 다루기로 하겠습니다



0개의 댓글