[React Basic] React의 Hooks (feat. useMemo, useCallback, useRef)

Joah·2022년 9월 23일
0

React Basic

목록 보기
17/25

게시글은 리액트 공부용이며 출처를 제시합니다.
출처: 소플의 처음 만난 React, 리액드를 다루는 기술, 모던 자바스크립트 deep dive

🛼 useMemo

useMemo() 훅은 Memoized value를 리턴하는 훅이다.
파라미터로 Memoized value를 생성하는 create함수와 의존성 배열을 받는다.

의존성 배열에 들어있는 변수가 변했을 경우에만 새로 create 함수를 호출하여 결괏값을 반환하며, 그렇지 않은 경우에는 기존 함수의 결괏값을 그대로 반환한다.

const memoizedValue = useMemo(
	()=>{
    	//연산량이 높은 작업을 수행하여 결과를 반환
      return computerExpensiveValue(의존성 변수1, 의존성 변수2)
    },[의존성 변수1, 의존성 변수2]);

useMemo() 훅을 사용하면 컴포넌트가 다시 렌더링될 때마다 연산량이 높은 작업을 반복하는 것을 피할 수 있다. 결과적으로는 빠른 렌더링 속도를 얻을 수 있다.

주의사항

🚫 useMemo() 훅을 사용할 때 useMemo()로 전달된 함수는 렌더링이 일어나는 동안 실행된다는 점이다. 따라서 렌더링이 일어나는 동안 실행돼서는 안될 작업을 useMemo 함수에 넣으면 안된다. 예를 들어 useEffect 훅에서 실행해야하는 사이드 이펙트 같은 것이 있다.

서버에서 데이터를 받아오거나 수동으로 DOM을 변경하는 작업 등은 렌더링이 일어나는 동안 실행돼서는 안되기 때문에 useMemo() 훅의 함수에 넣으면 안 되고 useEffect 훅을 사용해야 한다.

의존성 배열을 넣지 않으면 렌더링이 일어날 때마다 매번 함수가 실행되기 때문에 useMemo()사용하는 의미가 없다. 빈 배열을 넣게 되면 컴포넌트 마운트 시에만 함수가 실행된다.

예제

리스트에 숫자를 추가하면 추가된 숫자들의 평균을 보여주는 함수 컴포넌트

import React, { useState } from "react";

const getAverage = (numbers) => {
  console.log("평균 계산 중..");
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState("");

  const onChange = (e) => {
    //input에 적히는 내용을 가져와 number에 업데이트
    setNumber(e.target.value);
  };

  const onInsert = (e) => {
    //버튼을 클릭하면 새로운 리스트를 만들어 list를 업데이트 한다.
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };
  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {getAverage(list)}
      </div>
    </div>
  );
};

export default Average;
  • 콘솔을 열어보면 input 창에 값을 입력할 때마다 state가 업데이트 되며 그때마다 평균을 구하는 함수 getAverage가 호출된다.

useMemo를 사용하여 작업을 최적화 하고 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산을 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용해보자

 const avg = useMemo(() => getAverage(list), [list]);
  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );
  • getAverage 함수를 useMemo로 감싸고 list를 의존성 배열에 넣어 list가 업데이트 될 때만 getAverage 함수가 호출된다.

🛼 useCallback

useCallback() 훅은 useMemo() 훅과 유사한 역할을 한다. 한 가지 차이점은 값이 아닌 함수를 반환한다.
함수와 의존성 배열ㅇ르 파라미터로 받는다. 파라미터로 받는 함수를 콜백이라고 부른다. 의존성 배열에 있는 변수 중 하나라도 변경되면 Memorized된 콜백 함수를 반환한다.

const MemoizedCallback = useCallback(
	()=> {
    	doSomething(의존성 변수1, 의존성 변수2);
    },[의존성 변수1, 의존성 변수2]);

useCallback() 훅을 사용하여 특정 변수의 값이 변한 경우에만 함수를 다시 정의하도록 해서 불필요한 반복 작업을 없애준다.

예를 들어 useCallback() 훅을 사용하지 않고 컴포넌트 내에서 정의한 함수를 자식 컴포넌트에 props로 넘겨 사용하는 경우, 부모 컴포넌트가 다시 렌더링이 될 때마다 매번 자식 컴포넌트도 다시 렌더링된다.

하지만 useCallback() 훅을 사용하면 특정 변수의 값이 변한 경우에만 함수를 다시 정의하게 되므로, 함수가 다시 정의되지 않는 경우에 자식 컴포넌트도 리렌더링이 일어나지 않는다.

예제

useMemo에서 생성한 onChange와 onInsert 함수는 컴포너트가 리렌더링될 때마다 새로 만들어진 함수를 사용하게 된다.

대부분의 경우 문제 없지만 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이 부분을 최적화 하는게 좋다.

const onChange = useCallback((e) => {
    setNumber(e.target.value);
  }, []);

  const onInsert = useCallback(
    (e) => {
      const nextList = list.concat(parseInt(number));
      setList(nextList);
      setNumber("");
    },
    [number, list]
  );

첫 번째 파라미터에는 생성하고 싶은 함수, 두 번째 파라미터에서는 배열을 넣으면된다.
이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시한다.

onChange 처럼 비어 있는 배열을 넣게 되면 컴포넌트가 렌더링될 때 만들었던 함수를 계속해서 재사용한다.

onInsert 처럼 배열 안에 number와 list를 넣게 되면 인풋 내용이 바뀌거나 새로운 항목이 추가될때 새로 만들어진 함수를 사용하게 된다. onInsert 같은 경우에는 number와 list를 조회해서 nextList를 생성하기 때문에 배열 안에 number와 list를 꼭 넣어주어야 한다.


Memoization

컴퓨터 분야에서 메모이제이션은 최적화를 위해 사용하는 개념이다. 비용이 높은 함수의 호출 결과를 저장해 두었다가 같은 입력값으로 함수를 호출하면 새로 함수를 호출하지 않고 이전에 저장해놨던 호출 결과를 바로 반환한다. 이렇게 하면 결과적으로 함수 호출 결과를 받기까지 걸리는 시간도 짧아질뿐더러 불필요한 중복 연산도 하지 않게된다.


🛼 useRef

useRef() 훅은 레퍼런스를 사용하기 위한 훅이다.
리액트에서 레퍼런스란 특정 컴포넌트에 접근할 수 있는 객체를 의미한다.

useRef()훅은 바로 레퍼런스 객체를 반환한다. 레퍼런스 객체에는 .current라는 속성이 있는데 이것은 현재 레퍼런스(참조)하고 있는 엘리먼트를 의미한다.

const refContainer = useRef(초깃값)

파라미터로 들어온 초깃값으로 초기화된 레퍼런스 객체를 반환한다.
만약 초깃값이 null이라면 .current의 값이 null인 레퍼런스 객체가 반환된다.

이렇게 반환된 레퍼런스 객체는 컴포넌트 라이프타임 전체에 걸쳐서 유지된다.
즉, 컴포넌트가 마운트 해제 전까지는 계속 유지된다.

따라서
useRef()훅은 변경 가능한 .current라는 속성을 가진 하나의 상자라고 생각하면 된다.

function TextInput(props){
	const inputElem = useRef(null);
  
  	const onButtonclick = () => {
    	//`current`는 마운트된 input element를 가리킴
      	inputElem.current.focus();
    };
  
  return (
  	<>
    	<input ref={inputElem} type="text"/>
    	<button onClick={onButtonClick}>Focus the input</button>
    </>  
  )
}

리액트에서 <div ref={ref}/> 라는 코드를 작성하면 node가 변경될 때마다 myRef의 .current 속성에 현재 해당되는 DOM node를 저장한다.

useRef()훅은 다양한 변수를 저장할 수 있다. 그게 가능한 이유는 일반적인 자바스크립트 객체를 리턴하기 때문이다.

물론 {current: ...}를 작성해서 직접 객체를 만들 수 있다.
하지만
useRef()훅은 매번 렌더링될 때마다 항상 같은 ref 객체를 반환한다.

주의사항

🚫 useRef()는 내부의 데이터가 변경되었을 때 별도로 알리지 않아 .current 속성을 변경하는 것은 리렌더링을 일으키지 않는다.

따라서 ref에 DOM node가 연결되거나 분리되었을 경우 어떤 코드를 실행하고 싶다면 callback ref를 사용한다.

useCallback을 사용하지 않으면 UI에 아무것도 나타나지 않는다.

import React, { useCallback, useState } from "react";
import "./App.css";

function App() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback((node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h2 ref={measuredRef}>hola, React</h2>
      <h2>The height of the header is {Math.round(height)} px</h2>
    </>
  );
}

export default App;

위의 코드에서는 레퍼런스를 위해 useRef() 훅을 사용하지 않고 useCallback() 훅을 사용하여 callback ref방식을 사용했다.

useRef() 훅을 사용하게 되면 레퍼런스 객체가 .current속성이 변경되었는지를 따로 알려주지 않기 때문이다. 하지만 callback ref방식을 사용하게 되면 자식 컴포넌트가 변경되었을 때 알림을 받을 수 있고, 이를 통해 다른 정보들을 업데이트할 수 있다.

예제 코드에서는 첫 h2 태그의 높이 값을 매번 업데이트하고 있다.
그리고 useCallback() 훅의 의존성 배열로 비어있는 배열을 넣었는데 이렇게 하면 h2 태그가 마운트, 언마운트될 때만 콜백 함수가 호출되며 리렌더링이 일어날 때에는 호출되지 않는다.


Average.jsx 예제

ref를 사용하여 컴포넌트에서 등록 버튼을 눌렀을 때 포커스가 인풋으로 넘어가도록 코드를 작성해보자

const inputEl = useRef(null);

const onInsert = useCallback(
  (e) => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
    inputEl.current.focus();
  },
  [number, list]
);

return (
	<input value={number} onChange={onChange} ref={inputEl} />
)

useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다.

profile
Front-end Developer

0개의 댓글