본문 바로가기

cs/react

[React] 리액트 훅

목차

1. 리액트 훅이란

2. useState

3. useEffect

4. useMemo

5. useCallback


리액트 훅이란

리액트 훅은 함수형 컴포넌트에 상태와 생명주기 기능을 사용할 수 있게 해주는 기능입니다.

 

함수형 컴포넌트는 순수해야한다는 가정으로 사용되었기 때문에 상태와 생명주기를 사용할 수 없었습니다. 상태 관리와 생명주기 기능이 필요하다면 클래스형 컴포넌트를 사용해야 했었습니다.

 

하지만 클래스형 컴포넌트는 코드의 구성이 어렵고 코드 최적화가 어렵다는 단점이 있었습니다. 리액트 훅의 등장으로 함수형 컴포넌트에서도 상태 관리와 생명주기 기능 사용할 수 있게 되었습니다. 그 결과 하나의 컴포넌트를 생명주기가 아닌 기능을 기반으로 하여 작은 단위로 나눌수 있게 되었고 상태 로직을 재사용할 수 있게 되었습니다.

 

리액트 훅 규칙

  • 컴포넌트의 최상위에서만 훅을 호출해야하며, 반복문과 조건문 등에서 호출하면 안됩니다.
    • 최상위에서 훅 호출시 리액트 훅의 동일하게 호출하는 것이 보장됩니다.
    • 훅 호출을 동일하게 호출해야하는 이유는, useState, useEffect 등의 훅이 여러번 호출되는 중에도 훅의 상태를 올바르게 유지할 수 있기 때문입니다.
      • react는 훅의 호출 순서로 어떤 것을 정의한 훅인지 구분하기 때문에 순서가 변경되면 훅 순서가 밀려서 버그가 발생합니다.
  • 훅은 함수형 컴포넌트에서 호출해야합니다.

 

리액트 훅 종류

https://velog.io/@minw0_o/%EB%A6%AC%EC%95%A1%ED%8A%B8-hook-%EC%B4%9D-%EC%A0%95%EB%A6%AC

react는 기본적으로 컴포넌트 상태 관리 useState, 컴포넌트 생명주기 관리 useEffect, 컴포넌트 간의 전역 상태 관리 useContext 훅을 제공합니다.

이후 훅은 기본 훅의 동작 원리를 모방해서 만들어 졌습니다.

  • useReducer - 상태 업데이트 로직을 reducer 함수에 따로 분리
  • useRef - 컴포넌트나 HTML엘리먼트를 레퍼런스로 관리
  • useImperativeHandle - useRef의 레퍼런스를 상위 컴포넌트로 전달
  • useMemo - 의존성 배열에 적힌 값이 변할 때 값을 캐싱
  • useCallback - 의존성 배열에 적힌 값이 변할 때 함수를 캐싱
  • useLayoutEffect - 실제 DOM 변경 후 브라우저가 화면을 출력하기 이전에 effect를 동기적으로 실행
  • useDebugValue - 사용자 정의(custom) 훅의 디버깅을 도움

그 중 많이 사용하는 몇 개의 훅의 정의와 동작 원리를 설명하겠습니다.


useState

useState는 함수형 컴포넌트의 상태를 관리를 담당하는 훅 기능입니다.

 

자세한 내용은 아래 내용을 참고해주세요.

useState의 사용법 및 동작원리


useEffect

useEffect는 함수형 컴포넌트의 생명주기 관리를 담당하는 훅 기능입니다.

컴포넌트의 렌더링이 동작한 후 특정 작업을 수행할 수 있게 해줍니다.

 

useEffect 구성요소

useEffect는 두 개의 인자로 구성되어 있습니다.

첫번째 인자는 콜백 함수로, useEffect의 동작을 정의합니다. 

두번째 인자는 의존성 배열로, useEffect가 동작할 타겟을 정의합니다.

의존성 배열 설명을 보태자면, 의존성 배열에 정의된 값에 의한 컴포넌트 렌더링 후에 useEffect가 호출됩니다.

 

첫번째 인자의 콜백 함수에 clean up 기능도 존재합니다. clean up기능은 컴포넌트 생명주기에서 설명드리겠습니다.

 

아래는 의존성 배열을 활용하는 방법입니다.

//의존성 배열 - 빈 배열
useEffect(() => {
    console.log("마운트시 1회 호출");
}, []);

빈 배열 정의시, 컴포넌트가 마운트된 후 딱 1번만 호출됩니다.

재조정시 컴포넌트의 엘리먼트 타입이 변경되어 새로운 fiber가 생성된다면 호출됩니다.

//의존성 배열 - 특정 상태값
useEffect(() => {
    console.log("count 상태 변경시 호출");
}, [count]);

특정 상태값 정의시, 정의된 값이 변경되어 컴포넌트가 렌더링된 후 호출됩니다.

//의존성 배열 - 정의x
useEffect(() => {
    console.log("모든 렌더링 시 호출");
});

의존성 배열이 정의하지 않을시, 컴포넌트가 모든 렌더링에 호출됩니다.

useEffect의 호출 시점

useEffect는 렌더링 시 호출된다는 글이 많아서 DOM을 구성되는 중에 호출되는 거라고 생각했어서 혼란스러웠었습니다.

useEffect의 호출 시점은 실제 DOM을 구성하고 브라우저에 출력하는 commit phase 이후 시점에 useEffect의 콜백이 비동기로 호출됩니다.

 

useState로 관리되는 상태인 count가 의존성인 useEffect 예시입니다.

const [count,setCount] = useState(false);

useEffect(() => {
    console.log("count 상태 변경시 호출");
}, [count]);
  1. setCount로 렌더링
  2. 컴포넌트의 JSX 반환 및 가상 DOM 생성
  3. 수정된 기존 fiber를 실제 DOM에 삽입
  4. 브라우저 출력
  5. useEffect 콜백 호출
    • 모든 재조정 과정이 끝나고 수정된 화면이 브라우저에 출력이 되서야 콜백이 호출
    • setCount로 상태 변경시 계속 호출

 

useEffect 주의사항

1. 개발자 모드에선 설정 로직 두 번 호출

react 18부터 개발자 모드인 strict mode가 기본값으로 설정되는데, 개발 모드에서만 useEffect 작업 전에 useEffect + clean up 작업이 실행됩니다. 즉, useEffect가 두 번 호출되는것으로 보입니다.

 

예시입니다.

function StrictModeTest() {
    const [location, setLocation] = useState(fale);
    
    console.log("렌더링 전 호출");
    
    useEffect(()=> {
        return() => {
            console.log("clean up!");
        }
    });
    
    useEffect(()=> {
        console.log("useEffect 호출!");
    },[location]);
}

 

2. 의존성 객체 또는 함수 사용 지양

const [count, setCount] = useState(0);
const testObj = {"testObj":"111"};
useEffect(() => {
    console.log("count useEffect!!!! : ",count);
}, [count]);

useEffect(() => {
    console.log("object useEffect!!!!");
}, [testObj]);

testObj값을 수정한적도 없는데 testObj라는 객체를 의존성 배열로 정의한 useEffect가 계속 호출되는것을 확인할 수 있습니다.

 

이유

객체와 함수는 렌더링 될때마다 새로운 메모리 주소로 변경됩니다. 그렇기에 react는 객체가 계속 바뀌는 것으로 판단하고 useEffect가 호출되는 것입니다.

 

해결

의존성에는 원시값(state, number, string)을 넣는 걸 지향하며 객체 값을 넣어야하는 경우 useMemo, useCallback으로 memoization으로 메모리 주소를 안정화 하는것이 효율적입니다.

3. useEffect에서 DOM 조작 지양

useEffect 내부에서 DOM을 조작하는 경우 브라우저의 깜빡임 현상이 발생할 수 있습니다.

 

이유

useEffect는 브라우저가 출력된 이후에 호출되기 때문에 useEffect에서 DOM을 조작하면 이상 현상으로 깜빡임이 발생합니다.

 

해결

useLayoutEffect는 실제 DOM이 구성된 이후, 브라우저 출력 이전에 호출되는 훅입니다. 즉, 화면이 그려지기 전에 DOM을 수정하기 때문에 깜빡임 현상을 방지할 수 있습니다.

렌더링 → DOM 업데이트 → useLayoutEffect → 브라우저 출력 → useEffect

useMemo

함수형 컴포넌트에서 결과를 메모리에 저장(캐싱, memoization). 이후 결과 반환을 위해 콜백 재실행이 아닌 메모리에서 결과를 가져오게 하여 컴포넌트의 성능을 최적화 하는데 사용되는 훅.


useMemo 구성요소

useMemo도 두 개의 인자로 구성되어 있습니다.

첫번째 인자는 콜 백입니다. 마운트 시 콜백 내부가 동작하고 그 결과만 캐싱한 후 동일한 요청시 캐싱 결과만 반환합니다.

두번째 인자는 의존성 배열입니다. useMemo가 의존하는 상태에 의한 렌더링 발생시 콜 백이 호출되도록 합니다.

의존성 배열의 사용은 useEffect와 동일하게 활용할 수 있습니다.

function caculate() {
    //반복문 10억회
}

const value = useMemo(()=> caculate(), []);

 

 

useMemo 호출 시점

useMemo는 컴포넌트의 생명주기 중 render phase에서 동작합니다. 즉, 컴포넌트가 렌더링 되는 시점에 호출됩니다.

 

useMemo 활용

useMemo는 콜 백에 정의되어있는 로직 또는 함수를 수행하고 반환되는 결과만 메모리에 캐싱합니다.

 

예시입니다. 엄청 오래걸리는 함수가 있는데 그냥 컴포넌트 상위에 정의해 놓는다면 렌더링 시점마다 계속 호출되서 화면이 브라우저에 출력되는데 오래걸리는 비효율이 발생할 것입니다.

function Component() {
	const value = caculate();
	
	function caculate() {
		//반복문 10억회
		//반환 시간 10분
	}
	
	return (
		<div>{value}</value>
	)
}

 

 

하지만 그 함수를 useMemo를 이용해 결과를 저장해 놓는다면, 해당 결과만 메모리에서 꺼내 쓰면 이전 방법보다 더 효율적으로 react를 사용할 수 있을 것입니다.

function Component() {
	const value = useMemo(()=> caculate(), []);
	
	function caculate() {
		//반복문 10억회
		//반환 시간 10분
	}
	
	return (
		<div>{value}</div>
	)
}

 

 

만약 의존성 배열에 상태가 정의되어 있다면, 마운트 시점에 함수가 실행되서 결과를 캐싱하고, 해당 상태에 의해 리렌더링 된다면 그때 함수가 다시 호출되고 결과만 메모리에 저장될것입니다.

function Component() {
	const value = useMemo(()=> caculate(), [item]);
	
	function caculate() {
		//반복문 10억회
		//반환 시간 10분
        //item변수 사용
	}
	
	return (
		<div>{value}</div>
	)
}

 

의존성 배열로 인한 useMemo 동작은 아래와 같습니다.

마운트 → 함수 실행 후 결과 캐싱 → 캐싱된 결과 사용
→ 비의존성 상태로 인한 리렌더링 → 캐싱된 결과 사용
→ 의존성 변경으로 리렌더링 → 함수 실행 후 결과 캐싱 → 캐싱된 결과 사용

 

useMemo는 결과를 캐싱하여 동일한 결과를 메모리에서 가져오기 때문에 반복되는 결과를 효율적으로 사용할 수 있다는 점과 useEffect에서 사용하지 못하는 의존성 배열에 객체 참조가 가능해집니다.

 

의존성 배열 객체 참조

예시 입니다. 예시 코드는 해당 블로그에서 참조했습니다.

function UseEffect() {

    const [number, setNumber] = useState(1);
    const [isKorea, setIsKorea] = useState(true);

    // 1번 location
    const location = {
        country: isKorea ? "한국" : "일본"
    };

    // 2번 location
    //const location = useMemo(() => {
    //  return {
    //    country: isKorea ? '한국' : '일본'
    //  }
    //}, [isKorea])

    useEffect(() => {
        console.log("useEffect 호출!");
    }, [location]);

    return (
        <header className="App-header">
            <h2>하루에 몇 끼 먹어요?</h2>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(e.target.value)}
            />
            <hr />

            <h2>내가 있는 나라는?</h2>
            <p>나라: {location.country}</p>
            <button onClick={() => setIsKorea(!isKorea)}>Update</button>
        </header>
    )
}

출력 결과

객체로 정의된 location 변수, location 변수를 의존성 배열에 정의한 useEffect가 있습니다.

하루에 몇 끼 먹어요?라는 input박스는 number라는 상태를 관리하기 때문에 location객체와는 관련이 없기 때문에 useEffect 호출이 발생하지 않을 거라 생각했습니다.

하지만, 위의 출력 결과를 보면 location 변수를 의존성 배열로 갖는 useEffect가 호출된것을 확인할 수 있습니다.

이유는 위 useEffect주의 사항에서 설명했듯이 의존배열은 메모리 주소를 참조해서 변경 여부를 판단하는데, location변수가 참조하는 메모리 주소는 값 자체는 동일하지만 렌더링시 메모리 주소가 변경되기 때문에 useEffect는 location이 변경되었다고 판단하는 것입니다.

 

useMemo를 사용한 예시입니다.

function UseEffect() {

    const [number, setNumber] = useState(1);
    const [isKorea, setIsKorea] = useState(true);

    // 1번 location
    // const location = {
    //     country: isKorea ? "한국" : "일본"
    // };

    // 2번 location
    const location = useMemo(() => {
      return {
        country: isKorea ? '한국' : '일본'
      }
    }, [isKorea])

    useEffect(() => {
        console.log("useEffect 호출!");
    }, [location]);

    return (
        <header className="App-header">
            <h2>하루에 몇 끼 먹어요?</h2>
            <input
                type="number"
                value={number}
                onChange={(e) => setNumber(e.target.value)}
            />
            <hr />

            <h2>내가 있는 나라는?</h2>
            <p>나라: {location.country}</p>
            <button onClick={() => setIsKorea(!isKorea)}>Update</button>
        </header>
    )
}

 

number상태 변경 시
korea상태 변경 시

관련없는 상태인 number상태를 변경했을땐 useEffect가 호출되지 않았지만, korea상태를 변경했을땐 useEffect가 호출된것을 확인할 수 있습니다.

(처음 나온 2개의 로그는 stricMode모드에서 마운트 시점에 출력된 로그입니다.)

 

아래는 주요 핵심 코드입니다.

// 2번 location
const location = useMemo(() => {
  return {
    country: isKorea ? '한국' : '일본'
  }
}, [isKorea])

useEffect(() => {
    console.log("useEffect 호출!");
}, [location]);

 

마운트 시점에 location변수에 메모리에 캐싱된 객체를 저장합니다. number상태를 변경할때는 캐싱된 데이터를 가져오고 korea상태를 변경했을땐 객체가 수정되어 메모리에 수정된 데이터를 다시 캐싱합니다. 그렇기 때문에 렌더링 후에 useEffect는 location이 변경되었다고 판단하고 콜 백을 호출하게 되는 것입니다.


useCallback

useCallback도 메모이제이션 기법(캐싱)으로 컴포넌트 성능을 최적화 시켜주는 훅 기능입니다.

 

useCallback은 콜 백에 있는 함수 자체를 캐싱합니다.

 

useCallback 구성 요소

useCallback도 다른 훅들과 마찬가지로 두 개의 인자로 구성되어있습니다.

첫번째 인자는 콜 백으로, 콜 백 내부의 함수 자체가 메모리에 캐싱됩니다.

두번째 인자는 의존성 배열로, 정의된 값으로 인한 렌더링 발생시 콜 백이 호출됩니다.

 

의존성 배열은 빈 배열, 상태 정의, 정의 x로 다양하게 활용할 수 있습니다.

const handleClick = useCallback(() => {
  console.log("Clicked!");
}, []);

 

useCallback의 호출 시점

useCallback은 useMemo와 동일하게 컴포넌트 생명주기 중 render phase 시점에 호출되어 저장됩니다.

 

useCallback 활용

react는 렌더링 되면 컴포넌트의 함수를 먼저 실행하고, 변수를 초기화한다고 했습니다.

그렇기 때문에 그냥 함수를 정의한다면 매번 렌더링시 함수가 호출되는 비효율이 발생할 것 입니다.

그리고 매번 렌더링시 부모가 자식에게 함수를 props로 넘겨주거나 useEffect에 의존성 배열을 함수로 정의하면 자식과 useEffect는 계속 호출될 것입니다.

function Parent() {
  const handleClick = () => { ... }  // 렌더마다 새로 생성됨
}
<Child onClick={handleClick} /> //부모 렌더마다 자식도 렌더됨
useEffect(() => { ... }, [handleClick]) //렌더마다 호출

 

이런 비효율을 useCallback을 사용함으로써 렌더링시 변경되는 메모리 참조 주소를 방지할 수 있고, 자식 컴포넌트에 React.memo와 같은 기능과 함께 사용하면 자식 컴포넌트의 렌더링을 최소화하도록 효과적으로 사용할 수 있게 됩니다.

 

useMemo와 useCallback 차이

useMemo와 useCallback이 똑같은게 아닌가라고 헷갈렸었습니다.

useMemo는 콜 백에서 반환되는 결과만을 캐싱하는 방법.

useCallback은 콜 백 함수 자체를를 캐싱하는 방법.

 

예를들어 함수 실행 자체가 너무 오래 걸리는 경우 결과가 동일하다면 useMemo로 결과만 캐싱하고 함수에서 참고하는 상태가 변경되는 경우 의존성 배열로 선언해서 함수 호출을 최소화하는 방향으로 사용할 수 있습니다.

 

다른 예시로는, 부모가 자식에게 props로 함수를 전달할때, useCallback과 React.memo를 함께 활용한다면 부모가 전달하는 함수의 메모리 주소 변경을 최소화해서 자식 컴포넌트 리렌더링을 최소화하는 방향으로 사용할 수 있습니다.


참고

https://f-lab.kr/insight/understanding-react-hooks-20240626?gad_source=1&gad_campaignid=22368870602&gbraid=0AAAAACGgUFfkWLlEw3bDZTYByhBTbopdI&gclid=Cj0KCQiAxJXJBhD_ARIsAH_JGjiDCyYD1OvOAcxMrH9pwK_7KBCtvjS7gK6DHvWO9iC35m63X8q_IfAaAi18EALw_wcB

 

리액트 훅의 이해와 활용: useState와 useEffect

이 블로그 포스트는 리액트 훅의 중요성, useState 훅의 이해와 예제, useEffect 훅의 이해와 예제, useState와 useEffect의 조합, 그리고 리액트 훅의 결론을 다룹니다.

f-lab.kr

https://velog.io/@jinyoung985/React-useMemo%EB%9E%80

 

[React] useMemo란?

📋 useMemo란? useMemo는 리액트에서 컴포넌트의 성능을 최적화 하는데 사용되는 훅이다. useMemo에서 memo는 memoization을 뜻하는데 이는 그대로 해석하면 ‘메모리에 넣기’라는 의미이며 컴퓨터 프로

velog.io

https://ko.legacy.reactjs.org/docs/hooks-rules.html#explanation

 

Hook의 규칙 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

https://velog.io/@minw0_o/%EB%A6%AC%EC%95%A1%ED%8A%B8-hook-%EC%B4%9D-%EC%A0%95%EB%A6%AC

 

리액트 hook 총 정리

평소에 자주 사용해오던 리액트 Hook을 전체적으로 정리해본 적은 없어서 이번 기회에 한 번 전체적으로 정리해보려고 합니다🫠

velog.io

 

'cs > react' 카테고리의 다른 글

[React] Redux Toolkit  (0) 2025.12.01
[React] Context API & Provider 구조  (0) 2025.11.29
[React] 컴포넌트 생명주기  (0) 2025.11.28
[React] 가상 DOM과 재조정  (0) 2025.11.24
[React] react와 useState의 동작 원리  (0) 2025.11.21