Hook함수(2) useRef/useMemo/useContext/useCallback

hyerin·2023년 4월 29일
0

4. useRef

useRef는 React에서 참조할 수 있는 변수를 만들때 사용한다. 이 변수는 컴포넌트가 다시 렌더링되어도 값이 유지된다.
useRef의 hook은 useRef()함수를 사용하여 생성하며, 생성된 객체는 current 프로퍼티를 가집니다. current프로퍼티를 통해 DOM의 요소를 참조하거나, 컴포넌트 내부에서 값을 유지하고 관리할 때 쓰인다.

예제1 ) DOM 요소에 접근하기

다음은 버튼을 클릭하면 인풋에 포커스가 되는 예제이다. ref속성을 사용해 input이라는 태그에 접근하였다.

import React, { useRef, useEffect } from "react";

//useRef는 dom에 직접 접근하는 것이 가능함.
const Example6 = () => {

 //useRef의 ()안에 들어가 있는 것은 초기값이다
 //ref 속성에 할당하여 input 요소를 참조하게 된다.
  const inputRef = useRef();

  //빈배열을 넣어서 처음 렌더링 될때만 실행됨
  //맨처음 화면이 렌더링 될때 input에 focus가 잡힘
  //inputRef.current는 useRef훅으로 생성된 inputRef변수가 
  //참조하는 객체인(input)을 나타낸다.
  useEffect(() => {
    console.log(inputRef);
    //{current : undefined, useRef()에 초기값 설정 안함}
    inputRef.current.focus();
  }, []);

  //로그인되어 alert의 ok버튼을 누르면 자동으로 다시 input에 포커스됨
  const login = () => {
    alert(`환영합니다 ${inputRef.current.value}`);
    inputRef.current.focus();
  };
  return (
    <div>
        {/*inputRef가 참조하는 속성은 input이다*/}
      <input ref={inputRef} type="text" placeholder="username" />
      <button onClick={login}>로그인</button>
    </div>
  );
};

export default Example6;

예제2 ) 이전 값 유지하기

앞서 말했듯 useRef를 사용하여 생성한 객체의 current 프로퍼티에 값을 저장하면 해당 값은 컴포넌트가 리렌더링되어도 초기화되지 않는다. 이를 활용하면 이전 값과 차이를 비교한느 작업ㄷ을 수행할 수 있다

import React, {useRef, useState} from "react";
function Example(){
  const [count,setCount] = useState(0);
  const prevCountRef = useRef();
  
  useEffect(()=>{
  prevCountRef.current = count;
  },[count]);
  
  const prevCount = prevCountRef.current;
  
  return (
  <div>
      <p>Current count : {count}</p>
      <p>Previos count: {prevCount}</p>
      <button onClick={()=>{setCount(count+1)}}>
        Increase
      </button>
  </div>
  )
    }

5. useMemo

useMemo는 memorized value를 리턴(결과값을 반환)/하여 성능을 높이는 훅이다. useMemo의 가장 큰 특징은 값이 변경될때만 호출되고, 값이 변경되지 않은 경우에는 이전의 값을 캐싱하여 사용한다는 점이다.

의존성 배열로 들어간 변수가 변경되었을 경우에만 새로 함수를 호출하여 결과값을 반환한다.

컴포넌트가 렌더링 되면, 특정한 연산을 되풀이 하게 된다. 실제로 그 연산은 특정한 값이 바뀌지 않는 한 다시 연산할 필요가 없는데도 말이다.(어짜피 결과 값이 같은데 다시 검산하는 셈)

useMemo는 이전에 계산했던 결과를 return값으로 저장함으로써, 해당 의존성 배열의 값이 바뀌지 않는 이상 연산을 다시 수행하지 않고, 기존의 값을 뱉는다.

그래서 대부분의 경우에는 의존성 배열에 변수를 넣고 해당 변수들이 값이 바뀜에 따라 새로 값을 계산해야 될때 사용한다.

import React, { useMemo, useState } from "react";
const Sample = () => {
  //count라는 상태의 초기값은 0이다. 
  const [count, setCount] = useState(0);
  //sum함수는 count값이 변경될때만 호출된다.
  //count값이 변경되지 않은 경우 이전의 계산된 결과를 사용한다
  const sum = useMemo(() => {
    let result = 0;
    for (let i = 0; i < count; i++) {
      result += i;
    }
    return result;
  }, [count]);
  return (
    <div>
      <h2>
        {count}까지의 합: {sum}
      </h2>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </div>
  );
};

export default Sample;

6. useContext

useContext는 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 것을 도와주는 hook이다. useContext를 사용하면 props 전달을 순차적으로 하지 않아도, 해당 데이터가 필요한 자식 컴포넌트에서 해당 데이터를 사용할 수 있다.

  1. createContext로 context 생성
  2. 상위 컴포넌트를 `<Context이름.Provider>로 감싸기. 이때 주고 싶은 값을 value로 전달한다.
  3. 필요한 컴포넌트에서 useContext(Context이름) 을 변수명으로 가져와서 사용하기`

예제는 다음과 같다

import React, { useContext } from 'react';

// Context 생성
//()안에 들어간 값이 context의 기본값이 된다.
//value를 전달하지 않으면 기본값으로 context가 설정된다.
const ThemeContext = React.createContext('light');

// 상위 컴포넌트
//전달하고 싶은 값을 value로 전달한다.
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Header />
      <Main />
    </ThemeContext.Provider>
  );
}

// Header 컴포넌트에서 useContext 사용
function Header() {
  const theme = useContext(ThemeContext);
  return (
    <header className={theme}>
      <h1>Header</h1>
    </header>
  );
}

// Main 컴포넌트에서 useContext 사용
function Main() {
  const theme = useContext(ThemeContext);
  return (
    <main className={theme}>
      <h2>Main</h2>
    </main>
  );
}

useContext를 잘 사용하면 props를 사용하기 위해서 상위 컴포넌트에서부터 props를 전달해오는 props drilling을 피할 수 있다. 하지만 useContext의 경우 사용한 컴포넌트를 재사용이 힘들게 만들기 때문에 필요할 때만 사용하는 것이 좋다.

7.useCallback

useCallback은 기억시키고, 불필요한 렌더링을 방지하는 hook라는 점에서 useMemo와 유사하다. useCallback의 인자에 함수를 집어넣고, 이를 변수에 할당하면, 이 함수는 어디서든 재사용할 수 있는 함수가 된다.

인자로 전달한 함수 자체를 memoization 한다.
의존성 배열 안에 있는 값이 변경되지 않는 이상 항상 같은 함수(주소값도 같음)
를 props로 전달하여 재렌더링을 방지한다.

보통은 자식 컴포넌트에 함수를 props로 줄 때 사용한다.

function Component(){
const calculate = useCallback((num)=>{
return num+1;
},[item]);
  return <div>{value}</div>
}

위 예제에서 calculate에 useCallback을 활용한 함수를 저장해둔 것을 볼수 있다. num을 1 증가시키는 함수가 들어갔으며, 두 번째 인자로 들어간 item을 의존성 배열로 받아, item이 변경되지 않은 이상 초기화되지 않는다.
즉, useCallback은 item이 바뀌기 전 까지는 같은 함수이다. 같은 함수라는 것으 같은 주소값을
유지하고 있는 함수라는 뜻이다. 이 함수는 item이 바뀌기 전까지는 재렌더링 되지 않아 효율적인 렌더링을 가능하게 한다.

예제1

useCallback또한 불필요한 렌더링을 막을 수 있다는 장점이 있다. 다음 두 예제를 비교해보자. 밑의 예제는 useCallback을 사용하지 않은 함수이다. 버튼을 클릭할 때마다 state가 변경되고, 변경될때마다 렌더링이 일어나게 된다. 렌더링이 일어나면 그 안에 있는 someFunction 이라는 함수가 재실행이 된다. 이는 useEffect안에 someFunction을 의존성 배열로 넣은뒤 console을 찍으면 알 수 있다. 렌더링마다 Example10 에 있는 someFunction이 렌더링이 되고, 이를 의존성 배열로 받는 useEffect안의 console.log가 계속 실행되는 것이다.

someFunction은 컴포넌트인 example10이 재렌더링 될때마다 다시 렌더링될 필요가 없다. 어짜피 함수안의 내용은 동일하기 때문이다. 즉, 이 someFunction 함수가 다시 새로운 someFunction이라는 함수로 바뀌는 것을 막으려면, 기존에 있는 함수인 someFunction의 주소값을 useCallback을 통해 저장해 두어야 한다.

import { useEffect, useState } from "react";
const Example10 = () => {
  const [number, setNumber] = useState(0);
  const someFunction = () => {
    console.log(`sumFunc : number : ${number}`);
    return;
  };
  useEffect(() => {
    console.log(`someFunction이 변경되었습니다`);
  }, [someFunction]);

  return (
    <div>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(e.target.value)}
      />
      <br />
      <button onClick={someFunction}>Call someFunc</button>
    </div>
  );
};

export default Example10;

이러한 불필요한 렌더링을 막으려면 useCallback을 사용하여 최적화를 시킬 수 있다.

import { useEffect, useState, useCallback } from "react";
import Box from "./Box";

const Example11 = () => {
  const [number, setNumber] = useState(0);
  //일반함수가 아닌 useEffect을 사용
  //someFunction을 매번 렌더링될때마다 생성하는 것이 아니라 처음 렌더링할때 someFunction을 저장해 재사용하게 된다.
  const someFunction = useCallback(() => {
    console.log(`someFunc : number : ${number}`);
    return;
  }, []);

  useEffect(() => {
    console.log("someFunction이 변경되었습니다");
  }, [someFunction]);
  return (
    <div>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(e.target.value)}
      />
      <br />
      <button onClick={someFunction}>Call someFunc</button>
    </div>
  );
};
export default Example11;

useCallback을 사용하면 state가 바뀌어 매번 렌더링이 일어난다 하더라도, 일반함수와 달리 함수가 계속 만들어지지 않는다. 객체가 새로 생성되지 않는다는 말은, 그 객체의 주소값이 변하지 않는다는 말이다. useCallback은 의존성 배열로 넣은 객체의 주소값을 계속 저장함으로써 불필요한 렌더링을 방지한다.


위와 같은 화면이 렌더링 되는데, 화살표 위버튼으로 숫자를 계속 올려도 콘솔에는 그전과 같이 someFunction이 변경되었다는 콘솔이 찍히지 않는다. 이는 useCallback함수가 someFunction을 처음 저장하고 재사용하고 있기 때문에, 불필요한 함수의 재렌더링이 일어나지 않는 것이다.

만약 someFunction을 어떤 조건 하에서 다시 만들고 싶다면, 의존성 배열을 추가하면 된다. 위의 코드에서 somFunction을 number가 바뀔 때마다 함수를 바꾸고 싶다면, 두번째 인자로 number를 추가하자.

  const someFunction = useCallback(() => {
    console.log(`someFunc : number : ${number}`);
    return;
  }, [number]);

이러면 number가 바뀔 때마다 useCallback으로 만든 함수는 다시 선언된다!

예제2

다음은 input태그에 있는 숫자들로 <Box>의 크기를 자동적으로 조정하는 함수이다. 박스의 css인 createBoxStyle을 useCallback함수로 정의하였다. 이때 모드가 바뀔때는 이 함수가 실행되면 안되기 때문에, 의존성 배열로 [size]를 추가해주었다.

import React, { useState, useCallback } from "react";
import Box from "./Box";
const BoxExample = () => {
  //초기 사이즈는 100이다 
  const [size, setSize] = useState(100);
  const [isDark, setIsDark] = useState(false);

  //creatBoxStyle : css를 반환하는 객체,  Box에서 prop로 받은 size로 css를 변경시켜줌
  //size가 변경될때만 createBoxStyle을
  //재선언함 (useCallback의 의존성 배열 )
  const createBoxStyle = useCallback(() => {
    return {
      backgroundColor: "pink",
      width: `${size}px`,
      height: `${size}px`,
    };
  }, [size]);
  
  return (
    <div style={{ background: isDark ? "black" : "white" }}>
      {/*value안의 값이 size가 되고, 이 값으로 size를 바꾼다.*/}
      <input
        type="number"
        value={size}
        onChange={(e) => setSize(e.target.value)}
      />
      {/*모드를 변경해도 박스키우기가 작동되는 이유? 모드가 변경될때가 아니라 size가
      바뀔때만 size가 바뀌게 하는 법 ? 
      creatBoxStyle을 useCallback으로 감싸준다!
      */}
      <button onClick={() => setIsDark(!isDark)}>모드 변경</button>
      {/*box컴포넌트에 props를 전달한다. */}
      <Box createBoxStyle={createBoxStyle} />
    </div>
  );
};

export default BoxExample;

<Box>는 외부에서 정의된 컴포넌트이다. Box는 부모 컴포넌트인 BoxExample에서 createBoxStyle이라는 이름으로 useCallback으로 만든 createBoxStyle 이라는 함수를 전달한다.

가장 큰 컴포넌트인 BoxExample이 어떠한 이유로 렌더링이 되면, 안에 있는 createBoxStyle이라는 함수는 다시 정의되게 되고, 이를 인자로 받고 있는 Box마저도 props가 바뀌기 때문에 재렌더링이 된다. 분명 createBoxstyle의 내용은 바뀌지 않았는데 말이다. 부모 컴포넌트가 렌더링 되었다는 이유로 그 함수를 props로 받는 자식 컴포넌트까지 렌더링 되는 것은 매우 비효율적이다.

그래서 만약 size가 바뀌지 않는다면, 이 callback함수를 저장해서 그 함수를 props로 받는 자식 컴포넌트를 재렌더링되지 않게 하는 방법이 있다. 의존성 배열로 size를 추가해서, 만약 이 size가 바뀌지 않으면, createBoxStyled이라는 함수는 렌더링 되지 않고 기존의 함수를 불러온다(메모리 절약). 이를 props로 받는 Box또한 size라는 의존성 배열이 바뀔 때만 재렌더링 된다.

useCallback은 이렇게 함수를 인자로 받는 자식 컴포넌트의 무분별한 렌더링을 방지하기 위한 목적으로 많이 쓰인다.

profile
글쓰기의 시작은 나를 위해, 끝은 읽는 당신을 위해

0개의 댓글