TIL/[TIL]

[TIL]React Hooks 심화와 비동기 프로그래밍

namerong 2026. 4. 20. 15:37

1. useMemo

useMemo는 계산 결과를 메모이제이션하는 Hook이다.

의존성 배열에 들어 있는 값이 바뀌지 않으면 이전에 계산한 결과를 재사용한다.

const hardSum = useMemo(() => {
    return hardCalculator(hardNumber);
}, [hardNumber]);

정리

  • useMemo는 비용이 큰 계산을 매번 다시 수행하지 않도록 도와준다.
  • 의존성 배열의 값이 변경될 때만 콜백 함수를 다시 실행한다.
  • 불필요한 계산을 줄여 성능 최적화에 유용하다.

2. 값 메모이제이션

무거운 계산이 포함된 함수는 렌더링이 일어날 때마다 다시 실행되면 비효율적이다.

예제에서는 hardCalculator()가 긴 반복문을 포함한 무거운 계산이었고, easyCalculator()는 단순한 계산이었다.

const hardCalculator = (num) => {
    console.log('어려운 계산 시작');
    for (let i = 0; i < 19999999; i++) {}
    return num + 10000;
};
const hardSum = useMemo(() => {
    return hardCalculator(hardNumber);
}, [hardNumber]);

이렇게 하면 easyNumber가 바뀌어도 hardCalculator()는 다시 실행되지 않는다.

정리

  • 무거운 계산은 필요한 경우에만 다시 실행되도록 제어해야 한다.
  • useMemo는 계산 결과를 저장해 두고 재사용한다.
  • 모든 계산에 무조건 쓰는 것이 아니라, 실제로 성능 부담이 있는 경우에 사용하는 것이 좋다.

3. 참조형 데이터와 useMemo

객체나 배열은 내용이 같아 보여도 새로 생성되면 참조값이 달라진다.

React에서는 참조값이 바뀌면 다른 값으로 인식할 수 있기 때문에, useEffect나 자식 컴포넌트에 불필요한 변화가 발생할 수 있다.

const location = useMemo(() => {
    return { country: isKorea ? '한국' : '외국' };
}, [isKorea]);
useEffect(() => {
    console.log('useEffect 호출됨... location이 변경되었습니다.');
}, [location]);

정리

  • 객체와 배열은 참조형 데이터이다.
  • 같은 내용이어도 새 객체를 만들면 참조가 달라진다.
  • useMemo를 사용하면 필요한 시점에만 새 객체를 생성할 수 있다.
  • 참조 안정성이 중요할 때 useMemo가 유용하다.

4. useCallback

useCallback은 함수를 메모이제이션하는 Hook이다.

컴포넌트가 다시 렌더링될 때마다 함수가 새로 만들어지는 것을 방지할 수 있다.

const printNumber = useCallback(() => {
    console.log('current number: ', number);
}, [number]);

정리

  • useCallback은 함수 자체를 저장해 재사용한다.
  • 의존성 배열의 값이 바뀔 때만 새로운 함수를 만든다.
  • 함수 참조가 중요한 상황에서 성능 최적화에 도움이 된다.

5. 함수 메모이제이션이 필요한 이유

컴포넌트 안에서 함수를 일반적으로 선언하면 렌더링될 때마다 새로운 함수가 만들어진다.

const printNumber = () => {
    console.log('current number: ', number);
};

이 경우 useEffect의 의존성 배열이나 자식 컴포넌트 props로 전달할 때 불필요한 재실행이 발생할 수 있다.

useEffect(() => {
    console.log('printNumber 함수가 변경 되었습니다.');
}, [printNumber]);

정리

  • 함수도 객체처럼 참조값을 가진다.
  • 렌더링 때마다 새 함수가 만들어지면 참조가 계속 바뀐다.
  • 참조 안정성이 필요할 때 useCallback을 사용한다.

6. 자식 컴포넌트와 useCallback

부모가 자식에게 함수를 props로 전달할 때, 부모가 리렌더링될 때마다 함수가 새로 만들어지면 자식도 영향을 받을 수 있다.

function Square({ genSquareStyle }) {
    const [style, setStyle] = useState({});

    useEffect(() => {
        setStyle(genSquareStyle());
    }, [genSquareStyle]);

    return <div style={style}></div>;
}

부모에서 genSquareStyle을 useCallback으로 감싸면 size가 바뀔 때만 새 함수가 생성된다.

const genSquareStyle = useCallback(() => {
    return {
        backgroundColor: 'orangered',
        width: `${size}px`,
        height: `${size}px`
    };
}, [size]);

정리

  • 자식에게 전달하는 함수도 참조값이 중요하다.
  • useCallback을 사용하면 자식 컴포넌트의 불필요한 effect 실행을 줄일 수 있다.
  • 부모의 모든 상태 변화가 자식의 모든 로직을 다시 실행하게 두지 않도록 제어할 수 있다.

7. useRef

useRef는 리렌더링 없이 값을 저장하거나 DOM 요소에 직접 접근할 때 사용하는 Hook이다.

const countRef = useRef(0);

useRef는 .current 속성에 값을 저장한다.

countRef.current += 1;
console.log(countRef.current);

정리

  • useRef는 값을 유지하지만 값이 바뀌어도 리렌더링을 일으키지 않는다.
  • 상태처럼 화면에 바로 반영되는 용도가 아니라, 값을 보관하거나 DOM 참조를 다룰 때 적합하다.
  • .current에 실제 값이 저장된다.

8. useState, 일반 변수, useRef 차이

예제에서는 같은 증가 기능을 state, 일반 변수, ref 세 방식으로 비교했다.

8.1 useState

const [count, setCount] = useState(0);
  • 값이 변경되면 리렌더링이 발생한다.
  • 화면에 반영해야 하는 데이터에 적합하다.

8.2 일반 변수

let variable = 0;
  • 컴포넌트가 다시 렌더링되면 값이 초기화된다.
  • 렌더링 간 값을 유지하지 못한다.

8.3 useRef

const countRef = useRef(0);
  • 값을 유지할 수 있다.
  • 값이 바뀌어도 리렌더링되지 않는다.

정리

  • 화면에 반영되어야 하는 값은 useState
  • 리렌더링과 무관하게 유지해야 하는 값은 useRef
  • 일반 변수는 렌더링 사이에 값이 유지되지 않음

9. useRef로 DOM 접근하기

useRef는 실제 DOM 요소를 직접 참조할 때도 사용한다.

const usernameRef = useRef();
<input
    type="text"
    name="username"
    ref={usernameRef}
/>

마운트 직후 자동으로 포커스를 주거나, 버튼 클릭 후 다시 포커스를 줄 수 있다.

useEffect(() => {
    usernameRef.current.focus();
}, []);
usernameRef.current.focus();

정리

  • ref 속성으로 DOM 요소를 연결할 수 있다.
  • current를 통해 실제 DOM 객체에 접근한다.
  • 자동 포커스, 스크롤 이동, 텍스트 선택 등에 자주 사용된다.

10. Custom Hook

custom hook은 여러 컴포넌트에서 재사용할 수 있는 Hook 로직을 함수로 분리한 것이다.

이름은 반드시 use로 시작해야 한다.

function useDelayTimer(delay) {
    const [isReady, setIsReady] = useState(false);

    useEffect(() => {
        const timer = setTimeout(() => {
            setIsReady(true);
        }, delay);

        return () => clearTimeout(timer);
    }, [delay]);

    return isReady;
}

컴포넌트에서는 일반 Hook처럼 사용할 수 있다.

const isReady = useDelayTimer(3000);

정리

  • custom hook은 Hook 로직을 재사용하기 위한 함수이다.
  • 반복되는 useState, useEffect 조합을 깔끔하게 분리할 수 있다.
  • 여러 컴포넌트에서 같은 로직을 공유할 수 있다.

11. 비동기 프로그래밍 개요

비동기 프로그래밍은 시간이 걸리는 작업이 끝날 때까지 전체 코드 실행을 멈추지 않고 다음 작업을 이어가는 방식이다.

대표적으로 setTimeout, API 요청, 파일 읽기 같은 작업이 비동기 처리 대상이다.

  1. callback 기반 비동기 처리
  2. Promise 기반 처리
  3. async/await 기반 처리

12. Callback 기반 비동기 처리

가장 기본적인 비동기 처리는 callback 함수를 넘겨주는 방식이다.

function sayHello() {
    console.log('3초 뒤에 음식이 나왔습니다.');
}

console.log('햄버거를 주문합니다.');
setTimeout(sayHello, 3000);
console.log('감자튀김을 주문합니다.');

또한 여러 비동기 작업을 순서대로 실행하려면 callback 안에 callback을 계속 중첩해야 한다.

increase(0, (result) => {
    increase(result, (result) => {
        increase(result, (result) => {
            console.log('모든 작업이 끝났습니다.');
        });
    });
});

정리

  • callback은 비동기 작업이 끝난 뒤 실행할 함수를 전달하는 방식이다.
  • 간단한 작업에는 쓸 수 있지만, 작업이 많아질수록 중첩이 깊어진다.
  • 이런 구조를 callback hell이라고 부른다.

13. Promise

Promise는 비동기 작업의 성공 또는 실패 결과를 미래에 전달하겠다는 약속 객체이다.

function increase(number) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const result = number + 10;

            if (result > 50) {
                return reject(new Error('숫자가 너무 큽니다!'));
            }

            resolve(result);
        }, 1000);
    });
}

Promise는 .then(), .catch(), .finally()로 처리할 수 있다.

increase(0)
    .then(number => {
        console.log(number);
        return increase(number);
    })
    .catch(error => {
        console.error(error);
    })
    .finally(() => {
        console.log('모든 작업이 끝났습니다.');
    });

정리

  • resolve는 성공, reject는 실패를 의미한다.
  • .then()은 성공 결과를 이어서 처리한다.
  • .catch()는 에러를 처리한다.
  • .finally()는 성공과 실패 여부와 상관없이 마지막에 실행된다.

14. async/await

async/await는 Promise 기반 비동기 코드를 더 동기 코드처럼 읽기 쉽게 작성하게 해준다.

async function run() {
    try {
        let result = await increase(0);
        console.log(result);

        result = await increase(result);
        console.log(result);
    } catch (error) {
        console.error(error);
    }

    console.log('모든 작업이 끝났습니다.');
}

정리

  • async 함수는 Promise를 반환한다.
  • await는 Promise가 끝날 때까지 기다린 뒤 결과값을 꺼낸다.
  • 복잡한 .then() 체인보다 가독성이 좋다.
  • 에러 처리는 try...catch로 할 수 있다.

15. Callback, Promise, async/await 비교

15.1 Callback

  • 가장 기본적인 비동기 처리 방식
  • 코드 중첩이 심해질 수 있음
  • 순차 작업이 많아질수록 읽기 어려움

15.2 Promise

  • 성공과 실패를 명확히 나눌 수 있음
  • .then() 체인으로 흐름을 연결 가능
  • callback hell을 완화할 수 있음

15.3 async/await

  • Promise를 기반으로 동작함
  • 동기 코드처럼 읽혀서 이해가 쉬움
  • 에러 처리도 직관적임

16. 오늘 배운 핵심 정리

  1. useMemo는 계산 결과나 참조형 데이터를 메모이제이션할 때 사용한다.
  2. 무거운 계산이나 참조 안정성이 필요한 객체 생성에서 useMemo가 유용하다.
  3. useCallback은 함수를 메모이제이션하여 불필요한 함수 재생성을 막는다.
  4. 부모가 자식에게 함수를 props로 넘길 때 useCallback이 특히 중요하다.
  5. useRef는 리렌더링 없이 값을 저장하거나 DOM 요소에 직접 접근할 때 사용한다.
  6. useState, 일반 변수, useRef는 값 유지 방식과 리렌더링 여부가 다르다.
  7. custom hook은 반복되는 Hook 로직을 재사용 가능한 함수로 분리하는 방법이다.
  8. callback 기반 비동기 처리는 중첩이 깊어지면 callback hell 문제가 생길 수 있다.
  9. Promise는 비동기 성공과 실패를 더 체계적으로 다룰 수 있게 해준다.
  10. async/await는 Promise 코드를 더 읽기 쉬운 형태로 작성하게 해준다.