useMemo와 useCallback은 공통적으로, 불필요한 렌더링을 막고 최적화하기 위해서 사용되는 리액트 훅이다. 이 둘을 이해하기 위해서 memoization(메모이제이션)을 알면 좋을 것 같다.
컴퓨터 프로그램이 동일한 계산을 반복해야 할 때 똑같은 계산을 반복 수행하는 것이 아닌, 이전에 계산한 값을 메모리에 저장해두고 사용함으로써 프로그램 실행 속도를 빠르게 하는 기술을 의미한다.
React는 state가 변경될 때마다 다시 렌더링 되는데, 간혹 필요없는 것들을 계속해서 불러오곤 한다. 이는 자원의 낭비로 이어질 수 있고, 더 복잡한 서비스에서는 성능을 저하시키는 요인이 된다. 따라서 특정 상황에만 특정 동작을 하도록 유도할 필요가 있는데, 이때 useMemo
라는 hook을 사용하여 성능을 최적화할 수 있다.
const memoizedValue = useMemo(()=> computeExpensiveValue(a,b), [a,b]);
useMemo (function, deps);
function: 어떻게 연산할지 담는 함수
deps: 검사할 특정 값을 담은 배열
배열 안의 값이 바뀌면 함수를 호출하여 연산하고, 값이 바뀌지 않았다면 이전에 연산한 값을 재사용한다
메모이제이션된 값을 반환한다. 의존성(dependency)가 변경되었을때만 메모이제이션된 값을 다시 계산한다. 이를 통해 불필요한 연산을 줄일 수 있다.
문제?
input 값이 변할때 (string의 state 변화로 다시 렌더링 될 때) 마다 sum()
함수가 호출된다 이 sum()
함수는 stringList에 새로운 문자열이 추가될때에만 불려지는 함수인데, 계속 호출되므로 비효율적이라고 할 수 있다. 이때 useMemo
를 적용해보겠다.
적용
stringList가 변경될때에만 sum()
함수가 호출되도록 했습니다. 첫번째 파라미터에는 어떻게 연산할지 정의하는 함수, 두번째 파라미터에는 deps배열 안에 stringList
를 넣어줬습니다. 이 배열 안에 넣은 내용이 바뀌면 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용하게 되는 것이죠.
const result = useMemo(()=> sum(stringList), [stringList]);
return (
<div>
<input type="text" onChange={(e) => {setString(e.target.value)}/>
<button onClick={insert}>문자열 추가</button>
{result}
</div>
)
useCallback
은 useMemo
와 비슷한 함수이다. React에서 컴포넌트가 다시 렌더링될때 컴포넌트 안에 선언된 함수들을 새로 생성된다. 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야할 컴포넌트의 개수가 많아지면 최적화해주는 것이 효율적이며 이때 useCallback
hook을 활용할 수 있다.
const memoizedCallback = useCallback (()=>{
doSomething(a,b);
}, [a,b]);
메모이제이션된 콜백을 반환
메모이제이션된 콜백은 콜백의 의존성(dependency)이 변경되었을때만 변경된다
useCallback (function, deps);
function: 생성하고 싶은 함수
deps: 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시하는 배열
🎈 두번째 파라미터가 빈배열 []
일 경우
deps 배열이 비어있는 경우, 컴포넌트가 렌더링될 때 만들었던 함수를 계속해서 재사용하게 된다. 반대로, 배열 안에 값이 있으면 해당 값들이 변경될때 새로 만들어진 함수를 사용하게 되는 것이다.
아까와 동일한 예시입니다. 이 예시에서 insert
, handleChange
, sum
등의 함수는 렌더링될때마다 계속 재생성된다.
이러한 비효율적인 문제를 해결하기위해 useCallback
hook을 사용할 수 있다.
이렇게 각 함수를 useCallback
으로 감싸줬다.
handleChange
와 sum
함수는 최초로 렌더링될때만 함수가 생성되고 그 이후에서 생성되지 않는 반면, insert
함수는 deps 배열 안에 있는 string
과 stringList
가 변경될 때만 함수를 재생성한다.
정리하면
- 두번째 인자에 빈 배열인 경우: 최초의 렌더링 시에만 함수가 생성되고 이후에는 생성되지 않음 (어떤 상태값에도 반응하지 않음)
- 두번째 인자에 아무것도 넣지 않은 경우: 모든 상태변화에 반응
- 두번째 인자에 변수가 들어간 배열의 경우 : 해당 변수의 값이 변경될 때에만 함수를 재생성
즉, 해당 함수안에서 state를 사용할때 (특정 값에 의존할때) 반드시 두번째 인자인 배열 안에 해당 변수를 추가해줘야한다!
지금의 react hooks도 충분히 유용하지만, 자신만의 hook을 만들어서 사용하는 경우도 종종 있다. 자신이 필요한 부분에 있어서 기존의 hook이나 기능들을 사용하여 새로운 hook을 만들어 사용해볼 수 있다.
//LoginForm.js
import React, { useState, useCallback } from "react";
const LoginForm = () => {
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const onChangeId = useCallback((e) => {
setId(e.target.value);
}, []);
const onChangePassword = useCallback((e) => {
setPassword(e.target.value);
}, []);
return (
<input name="user-id" value={id} onChange={onChangeId}></input>
<input name="user-password" type="password" value={password} onChange={onChangePassword}></input>
)
}
export default LoginForm;
이와 같이 input의 값이 변경될 때 set함수를 통해서 값을 변경하는 비슷한 코드가 반복되는 경우가 있다.
useInput
이라는 이름으로 커스텀 훅을 만들어보겠다.
// hooks폴더 > useInput.js
import { useState, useCallback } from "react";
const useInput = (initialValue) => {
const [value, setValue] = useState(initialValue);
const handler = useCallback((e)=> {
setValue(e.target.value);
}, []);
return [value, handler];
};
export default useInput;
hooks폴더 안에 useInput
커스텀 훅을 만들었고, LoginForm
에서 사용해보겠다.
//LoginForm.js
import React, { useState, useCallback } from "react";
import useInput from "../hooks/useInput";
const LoginForm = () => {
const [id, onChangeId] = useInput("");
const [password, onChangePassword] = useInput("");
return (
<input name="user-id" value={id} onChange={onChangeId}></input>
<input name="user-password" type="password" value={password} onChange={onChangePassword}></input>
)
}
export default LoginForm;