인프런에서 리액트에서 타입스크립트를 사용하여 개발하는 내용을 보던중 내가 잘모르던 hooks를 좀 알아보려고 합니다.
React에서 컴포넌트가 다시 렌더링 될 때에는 컴포넌트 안에 선언된 함수들을 새로 생성합니다. 결국 계속해서 렌더링 되면 함수도 계속해서 새로 생성된다는 얘기입니다. 딱 들어봐도 비효율의 냄새가 나지 않습니까? 이런 비효율적인 작태를 React팀이 그냥 보고 넘어갔을 리 없겠죠. 그래서 나온것이 useCallback입니다. 저번 글의 useMemo와 비슷하게 퍼포먼스와 효율성을 위한 개념으로 만들어 졌습니다. 해당 기능을 사용하기 전의 코드를 먼저 보겠습니다.
함수형 컴포넌트는 그냥 함수다. 다시 한 번 강조하자면 함수형 컴포넌트는 단지 jsx를 반환하는 함수이다.
컴포넌트가 렌더링 된다는 것은 누군가가 그 함수(컴포넌트)를 호출하여서 실행되는 것을 말한다. 함수가 실행될 때마다 내부에 선언되어 있던 표현식(변수, 또다른 함수 등)도 매번 다시 선언되어 사용된다.
컴포넌트는 자신의 state가 변경되거나, 부모에게서 받는 props가 변경되었을 때마다 리렌더링 된다. (심지어 하위 컴포넌트에 최적화 설정을 해주지 않으면 부모에게서 받는 props가 변경되지 않았더라도 리렌더링 되는게 기본이다. )
메모리제이션된 값을 반환한다라는 문장이 핵심이다. 다음과 같은 상황을 상상해보자. 하위 컴포넌는 상위 컴포넌트로부터 a와 b라는 두 개의 props를 전달 받는다. 하위 컴포넌트에서는 a와 b를 전달 받으면 서로 다른 함수로 각각의 값을 가공(또는 계산)한 새로운 값을 보여주는 역할을 한다. 하위 컴포넌트는 props로 넘겨 받는 인자가 하나라도 변경될 때마다 렌더링되는데, props.a 만 변경 되었을 때 이전과 같은 값인 props.b도 다시 함수를 호출해서 재계산해야 할까? 그냥 이전에 계산된 값을 쓰면 되는데!
사용예시
import React, { useMemo } from "react";
const colorKor = useMemo(() => getColorKor(color), [color]);
const movieGenreKor = useMemo(() => getMovieGenreKor(movie), [movie]);
useMemo를 사용하면 의존성 배열에 넘겨준 값이 변경되었을 때만 메모리제이션된 값을 다시 계산한다.
지금 처럼 재계산하는 함수가 아주 간단하다면 성능상의 차이는 아주 미미하겠지만 만약 재계산하는 로직이 복잡하다면 불필요하게 비싼 계산을 하는 것을 막을 수 있다. (공식 문서에서도 useMemo에 넘겨주는 콜백 함수의 이름이 computeExpensiveValue() 라고 되어 있다는 것을 주목하자)
지금 나는 부모 컴포넌트에게 props를 전달 받는 상황으로 설명했지만, 하나의 컴포넌트에서 두 개 이상의 state가 있을 때도 활용할 수 있다.
메모리제이션된 함수를 반환한다라는 문장이 핵심이다. 서론에서 useMemo와 useCallback을 배우기 전에 알아야 하는 것들 중 두 번째로 컴포넌트가 렌더링 될 때마다 내부에 선언되어 있던 표현식(변수, 또다른 함수 등)도 매번 다시 선언되어 사용된다. 라고 이야기했다. useMemo를 설명하고 있는 같은 예제에서 App.js의 onChangeHandler 함수는 내부의 color, movie 상태값이 변경될 때마다 재선언된다는 것을 의미한다. 하지만 onChangeHandler 함수는 파라미터로 전달받은 이벤트 객체(e)의 target.id 값에 따라 setState를 실행해주기만 하면 되기 때문에, 첫 마운트 될 때 한 번만 선언하고 재사용하면 되지 않을까?
예시
// App.js
import React, { useState, useCallback } from "react";
const onChangeHandler = useCallback(e => {
if (e.target.id === "color") setColor(e.target.value);
else setMovie(e.target.value);
}, []);