4 - 2 useMemo와 useCallback 훅 이해하기
[ 1 ] 리액트 훅의 기본원리
리액트 훅 함수를 이해하려면 먼저 변수의 유효 범위에 관해 알아야 합니다. 예를들어 local은 블록 안에 서만 유효하고 블록을 벋어나면 자동으로 소멸합니다. 타입스크립트를 포함한 대부분의 프로그래밍 언어에서는 중괄호 { } 안쪽의 범위를 블록 범위라고 하고, 블록 범위 안쪽의 변수를 지역 변수라고 합니다.
변수와 블록 범위
{
const local = 1
}
지역 변수 개념은 다음처럼 함수 몸통에서도 똑같이 적용됩니다. 그런데 중괄호는 블록범위 이므로 return 문을 만나 블록을 벗어나면 그안에 선언한 변수가 자동으로 소멸합니다.
함수 몸통에서 변수의 유효 범위
function func() {
const local = 1
return local
}
리액트의 함수 컴포넌트 또한 '함수'이므로 몸통에서 변수의 유효 범위는 똑같이 적용됩니다. 다음 코드인 UserOrCreate 컴포넌트는 retrun 문으로 함수 몸통을 벗어나면 local 변수는 소멸합니다.
함수 컴포넌트에서 변수의 유효 범위
export default function UserOrCreate() {
const local = 1
return <p>{local}</p>
}
1. 상태와 캐시
프로그래밍 분야에서 상태란 용어는 변수의 유효 범위와 무관하게 계속 유지하는 값을 의미합니다. 그런데 상태는 한 번 설정되면 이후로는 값을 변경할 수 없는 '읽기 전용' 개념을 가진 불변 상태와 아무 때나 값을 변경할 수 있고 계속 유지하는 가변 상태로 나뉩니다.
그런데 함수 컴포넌트는 '함수'이므로 블록 범위라는 개념때문에 상태를 가질 수 없습니다.
이처럼 블록 범위 바깥의 변수를 전역 변수 라고 합니다.
함수 컴포넌트에서 전역 변수
const global = 1
export default function UseOrCreate() {
return <p>{global}</p>
}
이 코드는 global은 함수 몸통 바깥에 있으므로 소멸하지 않고 계속 유지됩니다.
리액트 훅은 상태를 가질 수 없는 함수 컴포넌트로 하여금 마치 상태를 가진 것처럼 동작할 수 있게 합니다. 그리고 이런 개념을 이용하면 캐시를 전역 변수 형태로 만들어서 구현할 수 있습니다. 캐시는 데이터나 값을 미리 복사해 놓는 임시 저장소를 의미합니다.
2. 캐시 구현하기

코드에서 cache는 전역 변수로 선언했습니다. 그리고 cache[key]에 설정한 값이 없을때는 callback 함수를 호출해 cache[key]에 저장할 값을 생성한 뒤 cache[key]에 저장합니다. 여기서 흥미로운 부분은 callback함수를 단 한 번만 호출 한다는 것입니다. 이는 앞서 알아본 캐시 개념과 정확히 일치합니다. 이제 이 함수를 테스트하는 UserOrCreateTest 컴포넌트를 만들어 보겠습니다
3. 캐시 사용하기
앞에 작성한 useOrCreate 함수를 사용해 head와 body란 이름으로 각기 다른 데이터를 캐시하는 코드입니다.
이 덕분에 head와 body는 CreateOrUseTest 컴포넌트가 렌더링 될 때마다 반복해서 생서되지 않고 컴포넌트가 생성될 때 한 번만 생성 됩니다. 따라서 코드를 실행해보면 중요한 데이터는 캐시를 이용하므로 화면이 다시 렌더링될 때 속도가 빠른 것을 느낄수 있습니다.


4. 캐시와 의존성 목록
리액트 프레임워크 내부에서 관리되는 캐시된 값은 어떤 상황이 일어나면 값을 갱신해 줘야 합니다. 리액트 훅에서는 캐시를 갱신하는게 하는 요소를 의존성 이라고합니다. 이러한 의존성으로 구성된 배열을 의존성목록이라고 합니다.
만약 이런 캐시 갱신이 필요없다면, 즉 의존성이 전혀 없다면 의존성 목록은 단순히 빈 배열 []을 사용하면 됩니다.
5. 함수 컴포넌트와 리액트 훅을 사용하는 이유
리액트는 컴포넌트의 속성값이 변할 때 항상 최신 값이 반영되도록 다시 렌더링해 줍니다. 그런데 컴포넌트 내부 로직에서 컴포넌트가 다시 렌더링되는 때는 리액트가 탐지하기 어렵습니다. 이 때문에 클래스 기반 컴포넌트는 다양한 메서드를 구현해서 렌더링 여부를 판단할 수 있게 합니다.
반면에 함수 컴포넌트에 리액트 훅을 사용하면 리액트 프레임워크가 의존성 목록에서 변한 값이 있는지만 판단하면 되므로 다시 렌더링 해야하는 때를 판단하기 쉽습니다.
[ 2 ] 데이터를 캐시하는 useMemo 훅
react 패키지는 데이터를 캐시하는 용도록 useMemo 훅을 제공합니다. 이 메모제이션(줄여서 memo라고 씀)은 과거에 계산한 값을 반복해서 사용 할 때 과거에 계산한 값을 캐시해 두는 방법으로 전체 계산속도를 높이는 코드 최적화 기법입니다.
userMemo 훅 임포트
import { useMemo } from 'react'
useMemo 훅을 사용하는 방법은 다음과 같습니다. useMemo뿐만아니라 useCallback, useEffect 훅은 의존성에 변화가 생길 때마다 콜백 함수를 자동으로 호출하여 의존성을 반영합니다. 보통 콜백 함수는 한 번만 호출되면 충분하므로 의존성 목록은 의존성이 전혀 없음을 의미하는 [ ]로 사용합니다.
useMemo 훅을 사용하는 방법
const 캐시된_데이터 = useMemo(콜백_함수, [의존성1, 의존성2 ...])
콜백_함수 = () => 원본_데이터
useMemo훅을 사용하는 타입스크립트 코드를 보면 다음과 같은 형태를 보이는 제네릭 함수입니다. 여기서 DependencyList는 읽기 전용 배열 입니다.
useMemo 훅 선언문
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
다음 코드는 useOrCreate 함수대신 useMemo 훅 함수로 컴포넌트를 구현한 것입니다.


[ 3 ] 콜백 함수를 캐시하는 useCallback 훅
onClick 콜백 함수는 ReactOnClick 컴포넌트가 렌더링될 때마다 계속 다시 생성되어 비효율적입니다. 이때 onClick을 useMemo훅에 캐시하면 좋을것 같지만 데이터를 캐시하는데 사용되므로 onClick 같은 함수를 캐시하지는 못합니다.
그래서 함수몸통을 캐시하는 useCallback훅을 제공합니다.
콜백 함수를 구현할 때 비효율 예
import type { SyntheticEvent } from 'react'
export default function ReactOnClick() {
const onClick = (e: SyntheticEvent) => { }
return (<button onClick={onClick}>Click Me</button>)
}
useCallback 훅 임포트
import { useCallback } from 'react'
useCallback 훅의 사용 개념은 useMemo와 같습니다. 다만 useMemo가 데이터를 캐시한다면 useCallback은 콜백 함수를 캐시한다는 차이가 있습니다.
useCallback 훅 사용법
const 캐시된_콜백_함수 = useCallback(원본_콜백_함수, 의존성_목록)
다음은 타입스크립트로 작성한 useCallback훅의 타입
useCallback 훅 선언문
function useCallback<T extends (...args: any[ ]) => any>(callback: T, deps: DependencyList): T;
그런데 useCallback훅의 타입 변수 T는 '(...args: any[ ]) => any'라는 타입 제약이 걸려 있습니다. 이는 타입 변수 T는 함수여야 함을 의미합니다.
쉽게 설명하자면
함수여야 한다: useCallback에 전달하는 값은 반드시 함수여야 해요.
즉, T는 함수 타입입니다.
인자와 반환값: 이 함수는 여러 개의 인자를 받을 수 있고, 반환값의 타입은 상관없습니다.
어떤 타입의 인자든 받을 수 있고, 어떤 타입의 값을 반환할 수 있습니다.
useCallback 사용 예(권장하지 않음)
const onClick = useCallback(() => alert('button clicked), [])
위 코드는 다음 코드와 같은 맥락이지만 이렇게 구현하면 callback 함수가 항상 새로 만들어 지므로 useCallback훅을 사용하는 의미가 퇴색됩니다.
useCallback 사용 예(권장)
const callback = () => alert('button clicked')
const onClick = useCallback(callback, [])
권장하지 않는 방법:
즉시 실행 함수는 코드의 가독성을 떨어뜨리고, 복잡한 로직을 처리하기 어려울 수 있습니다.
권장하는 방법:
별도의 함수를 정의하여 useCallback에 전달하면, 코드가 더 명확해지고 재사용성이 높아집니다. 또한, 외부 상태에 대한 의존성을 쉽게 관리할 수 있습니다.
위 코드에서 callback 함수의 타입은 () => void이고, 함수의 매개변수 부분()는 전개 연산자를 사용하면 (..,[])가 되므로 (...args: any[])에 해당합니다. 또한 함수의 반환 타입 void도 any의 자식 타입이므로 합당합니다.


버튼을 클릭할 때 alert이 호출되어 알림 창이 나타납니다. 그런데 어떤버튼을 클릭했는지 알려주지 않는 단점이 있습니다.
이를 해결하고자 고차 함수에 관해 알아보겠습니다.
1. 고차 함수 사용하기
고차함수는 다른 함수를 반환하는 함수를 의미합니다.
리액트 개발에서 고차 함수는 콜백 함수에 어떤 정보를 추가로 전달하려고 할 때 주로 사용합니다. 다음 그림은 앞서 본 Callback.tsx의 실행 결과와 달리 어떤 버튼을 클릭했는지 알 수 있습니다.
고차함수 예
const onClick = useCallback((name: string) => () => alert(`${name} clicked`), [ ])
리액트 프로그래밍에서 고차 함수를 구현하는 이유는 함수의 타입 불일치를 해결하기 위해서 입니다. onClick 이벤트 속성은 '() => void' 타입의 콜백 함수를 설정해야 하므로 다음 처럼 콜백 함수 내부에서 필요한 name 변수를 전달할 수 없습니다.
타입 불일치로 name 변수를 전달할 수 없음
() => alert(`${name} clicked`)
그러나 콜백 함수를 다음처럼 고차 함수로 구현하면 onClick이 요구하는 '() => void' 타입 함수를 반환할 수 있으면서 동시에 함수 몸통이 요구하는 name값 또한 전달 할 수 있습니다.
고차 함수 사용으로 타입 일치와 name 전달 가능
(name: string) => () => alert(`${name} clicked`)
예시)
const add = (a) => (b) => a + b;
const addFive = add(5);
console.log(addFive(3)); // 8
예시 2)
function a(ss) {
function b(dd) {
console.log(ss, dd);
} // a의 내용
}
이거랑 밑에꺼랑 같은
const aa = useCallback((ss) => (dd) => console.log(ss, dd), []);
고차 함수를 이용하는 코드로 수정해 보겠습니다.

