[SEB FE] Section4 unit 4 Hook, useMemo, useCallback

동화·2022년 11월 30일
0

코드스테이츠

목록 보기
29/32

Hook

  • 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드
  • class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다.
...
render(){
    /* 클래스 컴포넌트는 render() 안에서 변수를 작성할 수 있습니다. */
    const [counter, setCounter] = useState(0);
...
}

❗️클래스 컴포넌트에서는 useState() 훅을 호출할 수 없다는 에러




Hook 사용 규칙 ✔️


1. 리액트 함수의 최상위에서만 호출해야 한다. (반복문, 조건문, 중첩된 함수 내에서는 사용이 불가능)

...
if(counter) {
    const [sample, setSample] = useState(0);
}
...

count가 있을 때 sample이라는 state를 사용하고 싶어서 조건문 안에 useState() hook을 불러왔다고 가정 => ❌

컴포넌트 안에는 useState나 useEffect 같은 Hook들이 여러 번 사용될 수 있는데,
React는 이 Hook을 호출되는 순서대로 저장을 해놓는다.

그런데 조건문, 반복문에서 Hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고,
결국 예기치 못한 버그를 초래하게 될 수 있다.



  1. 오직 리액트 함수 내에서만 사용
...
window.onload = function () {
    useEffect(() => {
        // do something...
    }, [counter]);
}
...

window의 요소가 모두 준비가 되면 useEffect()가 호출되었으면 좋겠다고 생각해서 함수를 작성했다고 가정 => ❌

애초에 Hook은 React의 함수 컴포넌트 내에서 사용되도록 만들어진 메소드이기 때문에
근본적으로 일반 JavaScript 함수 내에서는 정상적으로 돌아가지 않는다.









useMemo 📋

function Calculator({value}){

	const result = calculate(value);

	return <>
      <div>
					{result}
      </div>
  </>;
}

렌더링 마다 위 함수를 계속 호출, 로딩시간이 길어지게 됨
값이 계속 바뀌는 게 아니라면, 어딘가에 저장해서 필요시마다 꺼내 쓸 수 있게 하는 것이 useMemo

/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import { useMemo } from "react";

function Calculator({value}){

	const result = useMemo(() => calculate(value), [value]);

	return <>
      <div>
					{result}
      </div>
  </>;
}

이런 식으로 useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 된다.



useMemo를 이용하여 앱 최적화하기

import React, { useState } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const answer = add(val1, val2);

  return (
    <div>
      <input
        className="name-input"
        placeholder="이름을 입력해주세요"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val1}
        type="number"
        onChange={(e) => setVal1(Number(e.target.value))}
      />
      <input
        className="value-input"
        placeholder="숫자를 입력해주세요"
        value={val2}
        type="number"
        onChange={(e) => setVal2(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

이때 이름을 입력할 때도 콘솔창에 계속 찍히는 걸 볼 수 있다

import React, { useState, useMemo } from "react";
import "./styles.css";
import { add } from "./add";

export default function App() {
  const [name, setName] = useState("");
  const [val1, setVal1] = useState(0);
  const [val2, setVal2] = useState(0);
  const answer = useMemo(() => add(val1, val2), [val1, val2]);

  ... 생략

useMemo(() => add(val1, val2), [val1, val2]);

val1, val2 값이 바뀌었을 때만 ()=> add(val1, val2)을 실행해 반환하고,

바뀌지 않았을 때는 이전에 계산했던 값을 그대로 사용한다는 의미를 갖는다.

여기까지 진행하고 나면, 이제 이름을 입력할 때는 콘솔에 찍히지 않는 걸 확인할 수가 있다







useCallback 🔙

useMemo는 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook

function Calculator({x, y}){

	const add = () => x + y;

	return <>
      <div>
					{add()}
      </div>
  </>;
}

이 add 함수는 props로 넘어온 x와 y 값을 더해

태그에 값을 출력하고 있고, 해당 컴포넌트가 렌더링 될 때마다 계속 새롭게 만들어 질 것이다.
이때, 해당 컴포넌트가 리렌더링 되더라도 바뀌지 않는 x, y 값이 있다고 가정하면 useMemo에서 했던 것처럼 어딘가에 저장해 그 값을 사용할 수 있게 하는 것이 useCallback이다.
useCallback을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다

/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import React, { useCallback } from "react";

function Calculator({x, y}){

	const add = useCallback(() => x + y, [x, y]);

	return <>
      <div>
					{add()}
      </div>
  </>;
}

그러나 useCallbackuseMemo처럼 효율적이지는 않다.
오히려 손해인 경우도 있음.
useCallback은 자식 컴포넌트의 props로 함수를 전달해줄 때 사용하기가 좋다.



useCallback과 참조 동등성

  • JavaScript에서 함수는 객체
  • 객체는 메모리에 저장할 때 값을 저장하는 게 아니라 값의 주소를 저장
  • 따라서 반환하는 값이 같을 지라도 일치연산자로 비교했을 때 false가 출력
function doubleFactory(){
    return (a) => 2 * a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // 16
double2(8); // 16
  
double1 === double2;  // false
double1 === double1;  // true

두개의 함수는 동일한 코드를 공유하더라도 메모리 주소가 다르기 때문에, 메모리 주소에 의한 참조 비교 시 다른 함수로 본다.




useCallback를 이용하여 앱 최적화하기

import { useState } from "react";
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = () => {
    return [input + 10, input + 100];
  };

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };

  return (
    <>
      <div style={theme} className="wall-paper">
        <input
          type="number"
          className="input"
          value={input}
          onChange={handleChange}
        />
        <button
          className={(light ? "light" : "dark") + " button"}
          onClick={() => setLight((prevLight) => !prevLight)}
        >
          {light ? "dark mode" : "light mode"}
        </button>
        <List getItems={getItems} />
      </div>
    </>
  );
}

옆의 button dark mode를 눌러도 “아이템을 가져옵니다.”가 콘솔에 출력된다.

❓ 버튼을 누를 때도 앱이 리렌더링 되므로, App 내부의 getItems() 함수가 다시 만들어진다.
새로이 만들어진 함수는 이전의 함수와 참조 비교시 다른 함수이기 때문에 List 구성 요소 내에서 useEffect HooksetItems를 호출하고 종속성이 변경됨에 따라 “아이템을 가져옵니다.”를 출력하는 것❗️

import { useState, useCallback } from "react";
import "./styles.css";
import List from "./List";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = useCallback(() => {
    return [input + 10, input + 100];
  }, [input]);
  
  ... 생략

0개의 댓글