[SeSAC Front-end] [React] Hooks 알아보기(useReducer, useMemo, useCallback)

Harimad·2022년 12월 6일
0

React

목록 보기
5/18
post-thumbnail

🌱 Intro

  • 이번 장에서는 React 의 대표적인 Hook인 useReducer, useMemo, useCallback에 대해서 알아보도록 하겠습니다.
  • 이 Hook 들은 리액트에서 최적화를 하는데 사용되는 함수들 입니다.
  • 어떻게 해서 최적화를 적용할 수 있는지 예시와 함께 살펴보겠습니다.
  • useMemo는 변수 메모, useCallback은 함수 메모, React.memo는 컴포넌트를 메모합니다.

🌱 1. useReducer


1-1. 특징

  • useReducer()는 컴포넌트의 상태를 관리할 때 사용하는 Hooks 입니다.

  • 특징

    • 컴포넌트 상태 업데이트 로직을 컴포넌트에서 분리 가능합니다.

    • 다른 컴포넌트에서도 해당 로직을 재사용 가능합니다.

    • 동작 과정

  • useReducer() 사용 방법

    const [state, dispatch] =useReducer(reducer, initialState);
    • state : 컴포넌트에서 사용할 상태 값
    • dispatch : 액션을 발생시키는 함수
    • reducer : reducer 함수
    • initialState : 초기 상태 값
  • dispatch() 사용 방법

    dispatch( {key: value} )
  • reducer()

    • 현재 상태(state)와 action 객체(업데이트를 위한 정보)를 인자로 받아와서 새로운 상태를 반환해주는 함수입니다.
    function reducer(state, action) {
     	// 새로운 상태를 만드는 로직
      return 새로운 상태;
    }

1-2. Counter Reducer 생성

  • countReducer.js
function countReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

export default countReducer;
  • Counter.js
import React, { useReducer } from "react";
import countReducer from './countReducer';

const Counter = () => {
  const [state, dispatch] = useReducer(countReducer, 0);
  
  function numUp() {
    dispatch( {type: "INCREMENT" });
  }
  
  function numDown() {
    dispatch( {type: "DECREMENT"});
  }
  
  retuern (
    <div>
      <p>
        현재 카운터 값은 <b>{state}</b>입니다.    
      </p>
      <button onClick={numUp}>+1</button>
      <button onClick={numDown}>-1</button>
    </div>
  );
};
export default Counter;
  • 실행결과

1-3. dispatch()에 여러 값 넣기

  • Counter.js
function numUp() {
  dispatch( {type: "INCREMENT", icon: "🏴"} );
}

function numDown() {
  dispatch( {type: "DECREMENT", icon: "🏴‍☠️"} );
}
  • CountReducer.js
function countReducer(state, action) {
  switch (action.type) {
   case 'INCREMENT':
     return action.icon;
    case 'DECREMENT':
      return action.icon;
    default: 
      return state;
  }
}
  • 실행결과

1-4. vs useState()

  • useState()
    • 컴포넌트에서 관리하는 값이 한 개
    • 값이 단순한 숫자, 문자열, 불리언 등의 값인 경우
  • useReducer()
    • 컴포넌트에서 관리하는 값이 여러 개
    • 구조가 복잡한 경우
  • 다음과 같은 데이터는 어떤 상태관리 함수가 좋을까요?
    {
     name: "harimad",
     age: 10,
     job: "teacher",
     skill: [
       {HTML : "상", Javascript: "중"},
       {Vue : "상", React: "하"}
     ]
    }
    • useState로 관리 할 수 있습니다. 다만, 코드가 길어지게 됩니다.
    • 그래서 상태관리를 useReducer()를 사용해서 다른 파일에 state를 다른 파일에 빼놓을 수 있습니다.
    • 결국 useState를 쓸지 useReducer를 쓸지는 개인의 프로젝트 규모나 상황에 따라서 선택해 써야합니다.

🌱 2. useMemo


2-1. 정의

  • useMemo()
    • 컴포넌트 최적화를 위해 사용하는 Hook 입니다.
    • 동일한 계산을 하는 함수를 포함하고 있는 컴포넌트가 반복적으로 렌더링 될 때, 해당 함수의 값을 메모리에 저장해 놓고 재사용할 수 있게 합니다.
  • 메모이제이션 (Memoization)
    • 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거합니다.
    • 프로그램 실행 속도를 빠르게 하는 기술입니다.

2-2. 왜 사용하는가?

  • 컴포넌트 안에 함수()가 존재 합니다.

  • 함수() 결과값을 출력하는 Component가 있습니다.

  • 만약, 렌더링이 되면 Componenet가 호출되면서 함수()가 또 호출됩니다.

  • 그러면 Component 의 return이 재실행 되면서 함수()의 결과값이 또 다시 출력하게 됩니다. (...👎)

  • 아래의 함수가 있다고 가정합니다.

    function MyComponent({a, b}) { // 부모로 부터 props로 a, b를 받습니다.
      const result = compute(a, b); // compute함수에 인자를 넣어서 값을 저장 합니다.
    
      return <div>{result}</div>; // 그 값을 화면에 출력 합니다. // 출력을 위해서는 compute함수가 실행될 때 까지 기다려야 합니다.
    }
    • 만약 compute() 함수가 매우 복잡한 연산을 수행한다면?
    • 따라서 결과 값을 리턴하는데 몇 초 이상 걸린다면?
      • 컴포넌트의 재랜더링이 필요할 때마다 compute()함수가 호출 됩니다.
      • 사용자는 지속적으로 UI에서 지연이 발생하는 것을 경험하게 됩니다.
  • 함수의 결과 값을 저장해서 재 렌더링이 되어도 함수 결과 값이 똑같다면, 이전에 저장한 함수 결과 값을 재활용 하면 좋지 않을까요?

  • 그것을 가능하게 하는 함수가 useMemo() 입니다.


2-3. 구조

  • useMemo() 구조
    const memoizaedValue = useMemo(
      () => {
        // 연산량이 많은 작업을 수행
        return compute(a, b)
      }, [a, b])
    • 첫 번째 매개변수
      • 콜백함수
      • 메모이제이션 할 값을 계산해서 반환해주는 함수
    • 두 번째 매개변수
      • 의존성 배열
      • 배열 안의 값이 업데이트 될 때만 콜백함수를 재호출 (useEffect와 유사)
  • 의존성 배열이 빈 배열인 경우
    useMemo(() => {
      // 연산량이 많은 작업을 수행
      return compute(a, b)
    }, []);
    • 컴포넌트가 마운트되었을 때만 콜백함수 호출합니다.
    • 이후에는 항상 같은 값을 가져와서 사용합니다.
  • 의존성 배열이 없는 경우
    useMemo(() => {
     // 연산량이 많은 작업을 수행
     return compute(a, b)
    });
    • 컴포넌트가 렌더링 될 때마다 콜백함수 호출합니다.
    • useMemo()를 사용하는 의미가 없습니다.(👎)
  • useMemo()를 적용하여 컴포넌트 수정
function MyComponent( {a, b} ) {
  const result = useMemo(() => compute(a, b), [a, b]); // 👈 compute() 함수를 useMemo()에 감싸줍니다.
  return <div>{result}</div>;
}
  • 이제 result는 처음 렌더링 될 때 compute() 함수가 1번 호출되어 결과 값을 메모하고,
  • a또는 b값이 바뀔 때만 compute() 함수가 호출되어 결과 값을 메모합니다.

2-4. useMemo를 사용한 계산

  • useMemoComponent.js
function hardCalculate(number) {
  console.log("어려운 계산중");
  for (let i = 0; i < 1000000; i++) {}
  
  return number + 10000;
}

const useMemoComponent = () => {
  const [hardNumber, setHardNumber] = useState(1);
  
  const hardSum = hardCalculate(hardNumber); // 👎
  
  return (
    <div>
      <h3>어려운 계산기</h3>
      <input 
        type="number" 
        value={hardNumber} 
        onChange={(e) => setHardNumber(parseInt(e.target.value))}>
      </input>
      <span> + 10000 = {hardNumber} </span>
    </div>
  );
}
  • 실행 결과

  • useMemoComponent.js

function hardCalculate(number) {
  console.log("어려운 계산중");
  for (let i = 0; i < 1000000; i++) {}
  
  return number + 10000;
}

function easyCalculate(number) {
  console.log("쉬운 계산중");
  return number + 1;
}

const useMemoComponent = () => {
  const [hardNumber, setHardNumber] = useState(1);
  const [easyNumber, setEasyNumber] = useState(1);
  
  const hardSum = hardCalculate(hardNumber); // 👎
  const easySum = easyCalculate(easyNumber);
  
  return (
    <div>
      <h3>어려운 계산기</h3>
      <input 
        type="number" 
        value={hardNumber} 
        onChange={(e) => setHardNumber(parseInt(e.target.value))}> // 👎 state가 변경 될 때 마다 hardCalculate() 호출
      </input>
      <span> + 10000 = {hardSum} </span>
      
	  <h3>쉬운 계산기</h3>
      <input 
        type="number" 
        value={easyNumber} 
        onChange={(e) => setEasyNumber(parseInt(e.target.value))}>
      </input>
      <span> + 1 = {easySum} </span>
    </div>
  );
}
  • 실행결과

  • useMemo()를 사용하여 수정

    const hardSum = useMemo(
      () => hardCalculate(hardNumber),
      [hardNumber]
    );
  • 실행결과

    • hardCalculate() 함수가 useMemo()에 의해서 값이 메모 되어 있기 때문에, 재렌더링이 되어도 hardCalculate() 함수가 재호출 되지 않습니다👍

  • useMemo 사용 시 주의점

    • 목적 : 값을 재사용하기 위해 별도의 메모리를 할당하여 값을 저장합니다.
    • 불필요한 값까지 메모이제이션하면 메모리 용량이 늘어나 성능 저하가 발생합니다.

🌱 3. useCallback


3-1. 정의

  • 이미 생성해 놓은 함수를 재사용할 수 있게 해주는 Hook 입니다.

  • useMemo()와 유사한 Hook 입니다.

  • 컴포넌트에 useCallBack()을 사용하지 않고 함수를 정의한 경우

    • 렌더링이 발생할 때마다 함수가 새로 정의됩니다.
    const MyComponent = () => {
      function myFunction() {
        console.log("함수 생성 완료") 
      }
    }
  • useCallBack() 구조

    const memoizedCallback = useCallBack(
      () => doSomething(a, b),
      [a, b]
    )
    • 첫 번째 매개변수

      • 콜백함수
    • 두 번째 매개변수

      • 의존성 배열
      • 배열 안의 값이 변경되면 함수를 새로 생성합니다.
  • 의존성 배열이 빈 배열인 경우

    useCallBack(() => {
      return  doSomething(a, b)
    }, []);
    • 컴포넌트가 마운트되었을 때만 함수를 새로 생성합니다.
    • 이후에는 항상 같은 함수를 재사용 합니다.
  • 의존성 배열이 없는 경우

    useCallBack(() => {
      return doSomething(a, b)
    });
    • 컴포넌트가 렌더링 될 때마다 함수를 새로 생성합니다.
    • useCallBack()를 사용하는 의미가 없습니다.

3-2. JS 함수 동등성

  • UseCallBackComponent1.js
const UseCallBackComponent1 = () => {
  const name1 = () => "soo";
  const name2 = () => "soo";
  
  console.log("name1 : ", name1);
  console.log("name2 : ", name2);
  
  return <div>{name1 === name2 ? "같다" : "다르다"}</div>
};

export default UseCallBackComponent1;
  • 실행결과

    • 함수가 객체라서 두 함수는 서로 다른 값입니다.

  • UseCallBackComponent2.js

const UseCallBackComponent2 = () => {
  const [count, setCount] = useState(0);
  
  const clickHandler = () => {
    console.log("count : ", count); 
  };
  
  return (
    <div>
      <input 
        type="number" 
        value={count} 
        onChange={ (e) => setCount(e.target.value) }
      />  
      <button onClick={clickHandler}>숫자 출력</button>
    </div>
  );
};

export default UseCallBackCompoenent2;
  • 실행결과
    • 리렌더링 되면 "렌더링 완료"가 계속 찍힙니다.

3-3. useEffect() 추가 1

  • UseCallBackComponent2.js
const UseCallBackComponent2 = () => {
  const [count, setCount] = useState(0);
  
  const clickHandler = () => {
    console.log("count : ", count); 
  };
  
  useEffect(() => {			   // 👈 추가
    console.log("렌더링 완료");
  });
  
  return (
    <div>
      <input 
        type="number" 
        value={count} 
        onChange={ (e) => setCount(e.target.value) }
      />  
      <button onClick={clickHandler}>숫자 출력</button>
    </div>
  );
};

export default UseCallBackCompoenent2;
  • 실행결과

3-4. useEffect() 추가 2

  • clickHandler 함수를 useEffect() 안의 두번째 인자의 요소로 넣어줍니다.
  • ❗ 하지만 함수는 useEffect()의 두번째 인자로 쓸 수 없습니다.
  • UseCallBackComponent2.js
const UseCallBackComponent2 = () => {
  const [count, setCount] = useState(0);
  
  const clickHandler = () => {
    console.log("count : ", count); 
  };
  
  useEffect(() => {			   // 👈 추가
    console.log("clickHandler() 변경");
  }, [clickHandler]);
  
  return (
    <div>
      <input 
        type="number" 
        value={count} 
        onChange={ (e) => setCount(e.target.value) }
      />  
      <button onClick={clickHandler}>숫자 출력</button>
    </div>
  );
};

export default UseCallBackCompoenent2;
  • 실행결과

    • 리렌더링 되어도 여전히 useEffect()의 콜백함수가 실행됩니다.
    • 🔥 왜냐하면 함수는 객체 이기 때문에 리렌더링 되면 새로운 함수가 생성됩니다. 이때 useEffect()는 새로운 함수값이 생성 되었다고 인식해서 useEffect()의 콜백함수가 실행됩니다.
    • 함수는 객체이기 때문에 서로 다른 주소에 저장됩니다.
  • UseCallBackComponent2.js

    • 함수를 useCallback()안의 콜백함수로 넣어준 변수를 useEffect()의 두번째 인자 값으로 넣어줍니다.
    • 그러면 리렌더링이 되더라도 useEffect()의 콜백함수가 실행되지 않습니다.
    • 왜냐하면 리렌더링 되더라도 useCallback()안의 콜백함수에 담은 함수는 메모 되어있기 때문입니다.
    const UseCallBackComponent2 = () => {
      const [count, setCount] = useState(0);
    
      const clickHandler = useCallback(() => { // 👈
        console.log("count : ", count)
      }, [] );
    
      useEffect(() => {
        console.log("clickHandler() 변경"); 
      }, [clickHandler]);
    
      // 생략
    };
    
    export default UseCallBackComponent2;
  • 실행결과


3-5. useCallback() 최종

const UseCallBackComponent2 = () => {
  const [count, setCount] = useState(0);
  
  const clickHandler = useCallback(() => {
    console.log("count : ", count)
  }, [count]);
  
  useEffect(() => {
    console.log("clickHandler() 변경");
  }, [clickHandler]);
  
  // 생략
};

export default UseCallBackComponent2;
  • 실행결과

3-6. +React.memo()

  • App.js
function App() {
  const [count, setCount] = useState(0);
  
  const updateHandler = () => {
    console.log(`update`)
  };
  
  return (
    <div>
      <input
        type="number"
        onChange={(e) => setCount(e.target.value)}
      />
      <ChildComponent update={updateHandler} />
    </div>  
  )
}

export default App;
  • ChildComponent.js
const ChildComponent = (props) => {
  const { update } = props;
  
  console.log("child component 렌더링");
  
  return <div></div>;
};

export default ChildComponenet;
  • 실행결과

3-7. +React.memo()-수정

  • App.js
function App() {
  const [count, setCount] = useState(0);
  
  const updateHandler = useCallback(() => {  // 👈useCallback() 추가
    console.log("update"); 
  }, []);
  
  return (
    <div>
      <input
        type="number"
        onChange={(e) => setCount(e.target.value)}
      />
      <ChildComponent update={updateHandler} /> // 👈 사용
    </div>  
  )
}

export default App;
  • 실행결과

  • useCallback()을 사용했지만 자식 컴포넌트가 호출 되는 이유?

    • 자식 컴포넌트가 호출 될 때 매번 새로운 props 객체가 생성되기 때문입니다.
    • ChildComponent.js
const ChildComponent = (props) => { // ❗❗ 실행 될 때마다 새로운 props객체 생성❗❗
  const { update } = props;
  
  console.log("child component 렌더링");
  
  return <div></div>;
};

export defualt ChildComponent;

3-8. +React.memo()-최종

-ChildComponent.js

import React, { memo } from "react"; // 👈

const ChildComponent = (props) => {
  const { update } = props;
  
  console.log("child component 렌더링");
  
  return <div></div>;
};

export default memo(ChildComponent); // 👈
  • 실행결과

3-8. React.memo()

  • 리액트에서 제공하는 고차 컴포넌트 입니다.
    • 컴포넌트를 인자로 받아서 새로운 컴포넌트로 반환해 줍니다.
  • props의 변화가 있는지를 체크합니다.
    • 변화가 있다면 렌더링을 수행합니다.
    • 변화가 없다면 기존에 렌더링 된 내용을 재사용합니다.

🌱 나가면서

  • 지금까지 useReducer(), useMemo(), React.memo()에 대해서 알아보았습니다.
  • 각각의 기능을 실제 리액트 프로젝트에 적용하면서 최적화를 해보면 성능을 향상하는 데에 도움이 됩니다.
  • 추가적인 자료는 참고란의 링크를 확인해보시길 바랍니다.

🌱 참고

profile
Here and Now. 🧗‍♂️

0개의 댓글