웹 개발

[React] 자주 사용하는 Hook (useState, useEffect, useRef, useMemo, useCallback, useCont

sdesign416 2026. 7. 18. 00:57

Hook이란?

Hook은 함수형 컴포넌트에서 리액트의 기능(State, 생명주기)을 사용할 수 있도록 제공하는 함수이다.

※ React 16.8 이후에 Hook이 등장하여 함수형 컴포넌트로 대부분의 기능을 구현할 수 있게 되었다.
 

💡 생명주기란?

리액트에서 생명주기란 컴포넌트가 생성되어 화면에 나타나고, 변경되고, 사라질때까지의 전체 과정을 말한다.

  • 컴포넌트가 처음 화면에 나타날 때 → Mount
  • State나 Props가 변경되어 다시 렌더링될 때 → Update
  • 컴포넌트가 화면에서 제거될 때  → Unmount

 

💡 자주 사용하는 Hook

대부분의 Hook는 use로 시작하는 이름을 가지고 있다.
사용자 정의 Hook(Custom Hook)를 만들때도 동일하게 use로 시작하도록 만들어주면 된다.

  • useState: 상태(state) 저장
  • useEffect: 렌더링 이후 작업 수행
  • useRef: DOM 접근 또는 값 저장
  • useMemo: 계산 결과 메모이제이션
  • useCallback: 함수 메모이제이션
  • useContext: Context 데이터 사용

1. useState

useState는 컴포넌트 안에서 값을 기억하고, 그 값이 변경되면 컴포넌트를 다시 렌더링하는 상태 관리 Hook이다.

const [count, setCount] = useState(0)
  • count: 현재 값
  • setCount: 값을 변경하는 함수
  • 0: 초기값

상태값을 직접 변경하는것이 아니라, 반드시 상태 변경 함수를 사용하여 변경해주어야 한다,

setCount(count + 1)

setCount() 가 실행되면 상태값이 변경되고, 리액트는 변경된 상태를 화면에 반영하기 위해 컴포넌트를 다시 렌더링한다.
주로 입력값, 체크박스 상태, 모달 열기와 닫기, 카운터처럼 화면에 표시되며 변경되는 데이터를 관리할 때 사용한다.


2. useEffect

useEffect는 컴포넌트가 렌더링된 이후에 추가 작업을 실행하거나, 외부 시스템과 컴포넌트를 동기화할때 사용하는 Hook이다.

useEffect(() => {
    실행할 작업
}, [의존성])

주로 아래와 같은 작업에 사용된다.

  • API를 호출하여 데이터 가져오기
  • 이벤트 등록
  • 타이머 설정
  • 외부 라이브러리 연결
  • WebSocket과 같은 외부 시스템 연결

의존성 배열에 들어있는 값이 변경되면 다시 useEffect가 실행된다.
아래와 같이 작성할경우 컴포넌트가 처음 화면에 나타날 때 한 번 실행되고, 이후 user 값이 변경될때마다 다시 실행된다.

useEffect(() => { 
    console.log("user가 변경되었습니다.") }, 
[user])

 

💡 의존성 배열에 따른 실행 차이

// [1] 빈 배열 전달: 컴포넌트가 처음 화면에 나타난 뒤 한 번 실행
useEffect(() => {
    실행할 작업
}, [])
// [2] 배열에 값 입력: 배열의 값(user)이 변경될 때마다 실행
useEffect(() => {
    실행할 작업
}, [user])

 

// [3] 의존성 배열을 생략: 컴포넌트가 렌더링될 때마다 실행
useEffect(() => {
    실행할 작업
})

 
 

💡 Cleanup 함수

userEffect 내부에서 함수를 반환하면 해당 함수는 이전 작업을 정리하는 Cleanup 함수가 된다.
Cleanup함수는 보통 두 상황에서 실행된다.

  • 의존성 값이 변경되어 다음 Effect가 실행되기 직전
  • 컴포넌트가 화면에서 제거될 때
useEffect(() => {
    console.log("현재 user 설정:", user)

    return () => {
        console.log("이전 user 정리:", user)
    }
}, [user])

 

useEffect 실행흐름

컴포넌트가 제거될때는 useEffect의 작업 내용이 다시 실행되는 것이 아니라, 마지막으로 등록된 Cleanup 함수만 실행된다.
이벤트를 등록했다면 이벤트를 제거하고, 타이머를 만들었다면 타이머를 종료하는 방식으로 이전 작업을 정리할 때 사용한다.


3. useRef

useRef는 컴포넌트 안에서 값을 기억해두는 상자같은 역할을 하는 Hook이다.
useRef에 저장된 값은 컴포넌트가 다시 렌더링되어도 유지된다.

// 기본 형식: 초기값이 0인 ref 생성
const countRef = useRef(0)

useRef(0)을 실행하면 {current: 0} 형태의 객체가 만들어진다.
useRef에 저장된 실제 값은 current라는 속성 안에 들어 있다.
따라서 값을 확인하거나 변경할 때는 .current를 사용해야 한다.
 
 

// 저장된 값 확인
console.log(countRef.current)

위 코드로 저장된 값을 확인할 수 있으며 현재 초기값은 0이기 때문에 콘솔에는 0이 출력된다.
 

// 저장된 값 변경: 현재 값에 1을 더해 다시 저장
countRef.current = countRef.current + 1
// 간단하게 작성
countRef.current += 1

이 코드가 실행되면 countRef.current의 값은 1이 된다.
하지만 useState와 달리 useRef의 값을 변경해도 컴포넌트가 다시 렌더링되지는 않기 때문에 아래 상황에서 주로 사용한다.
 

  • DOM 요소에 직접 접근
  • 이전 값 저장
  • 타이머 ID 저장
  • 렌더링 횟수 저장
  • 렌더링과 관계없는 값 유지

 

💡 DOM 요소 접근하기

아래 코드는 DOM요소에 직접 접근할 수 있도록 하는 useRef의 예제코드이다.
버튼을 클릭하면 inputRef.current를 통해 실제 input 요소에 접근하여 focus()를 실행할 수 있게된다.

import { useRef } from "react"

function InputFocus() {
    const inputRef = useRef(null)

    const handleFocus = () => {
        inputRef.current.focus()
    }

    return (
        <>
            <input ref={inputRef} />
            <button onClick={handleFocus}>
                입력창 선택
            </button>
        </>
    )
}

4. useMemo

useMemo는 계산된 결과값을 기억하는 Hook이다.
컴포넌트가 다시 렌더링되더라도 의존성 값이 변경되지 않았다면 이전에 계산한 결과값을 재사용한다.

// 기본형식
const 기억된값 = useMemo(() => {
    return 계산결과
}, [의존성])

위 코드처럼 작성할경우 배열을 필터링하거나 정렬하는 계산 결과를 기억할 수 있다.
 

// 사용예시
const filteredUsers = useMemo(() => {
    return users.filter((user) => user.active)
}, [users])

이때 의존성 배열에 users 값을 넣게되면 해당 배열 값이 변경될경우 필터링 작업을 다시 실행한다.
하지만 users가 변경되지 않았다면 이전에 계산한 fillteredUsers값을 그대로 사용한다.
 
useMemo는 보통 많은 데이터를 필터링, 계산하는 상황에서 사용할 수 있다.

  • 많은 데이터 필터링
  • 배열 정렬
  • 복잡한 계산
  • 계산 비용이 큰 작업
  • 같은 계산이 불필요하게 반복되는 경우

하지만 이런 상황일때마다 모든 값을 useMemo로 감싸는것은 좋은 방법이 아니다.
간단한 계산까지 useMemo를 사용하면 값을 저장하고 의존성을 비교하는 과정이 추가되어 오히려 코드가 복잡해진다.
그렇기때문에 계산 비용이 실제로 크고, 컴포넌트 렌더링이 자주 발생하는 상황에서만 사용하는것이 좋다.


5. useCallback

useCallback은 함수 자체를 기억하는 Hook이다.
리액트 함수형 컴포넌트는 상태(State)가 변경되어 다시 렌더링되면 컴포넌트 함수 전체가 다시 실행된다.
그래서 컴포넌트 안에 선언한 함수도 매번 새롭게 생성된다.

// useCallback Hook 사용 전
function App() {
    const handleAdd = () => {
        console.log("추가")
    }
    return (
        <Button onClick={handleAdd} />
    )
}

위 코드는 컴포넌트가 다시 렌더링될때마다 handleAdd가 생성되고 다시 렌더링되고 또 다시 새로운 handleAdd가 생성되고 또 다시 렌더링되는 방식이다.

이전 handleAdd !== 새로운 HandleAdd

이전 함수와 새로운 함수는 작성된 내용이 같더라도 서로 다른 메모리 위치에 만들어지기 때문에 같은 함수 객체가 아니다.
쉽게 말해, 내용은 같지만 메모리 주소가 다른 새로운 함수 객체가 생성되고 있는것이다.
 

// useCallback 사용
const handleAdd = useCallback(() => {
    console.log("추가")
}, [])

useCalback을 사용하면 의존성 값이 변경되지 않는 동안 이전에 생성한 함수 객체를 재사용할 수 있다.
처음 렌더링 → handleAdd 생성 → 다시 렌더링 → 의존성 변경 없으면 기존 handelAdd 그대로 사용
 
useCallback은 보통 아래와 같은 상황에서 사용된다.

  • 자식 컴포넌트에 함수를 Props로 전달할 때
  • React.memo를 사용하는 자식 컴포넌트의 불필요한 렌더링을 줄일 때
  • 다른 Hook의 의존성 배열에 함수를 넣어야 할 때

useCallback도 모든 함수에 사용하는것이 아닌,
함수를 새로 생성하는 비용이 크지 않거나, 함수 재생성이 자식 컴포넌트의 렌더링에 영향을 주지 않을 경우에는 사용하지 않는것이 더 좋다.


6. useContext

useContext는 Context에 저장된 데이터를 가져와 사용하는 Hook이다.
Context는 여러 컴포넌트가 함께 사용할 데이터를 저장하는 공간이다.
 
Context를 사용하지 않으면 깊이 있는 자식 컴포넌트까지 데이터를 전달하기 위해 중간 컴포넌트들이 계속 Props를 전달해야할수도 있다.

※ 부모 컴포넌트 → 중간 컴포넌트 → 중간 컴포넌트 → 실제 데이터가 필요한 자식 컴포넌트
※ Props Drilling: 중간 컴포넌트가 사용하지 않는 데이터까지 계속 전달하는 현상
 

Context를 사용하면 Provider가 데이터를 제공하고, 필요한 컴포넌트가 userContext를 통해 직접 데이터를 가져올 수 있다.
 

Context 생성

import { createContext } from "react"

const UserContext = createContext()

먼저 createContext()를 이용해 Context를 생성한다.
 

Provider로 데이터 저장

<UserContext.Provider value={user}>
    <App />
</UserContext.Provider>

생성한 Context의 Provider를 사용하여 데이터를 저장한다.
아래 코드에선 user 데이터를 Context에 저장하고 있다.
 

useContext 사용

import { useContext } from "react"

function Profile() {
    const user = useContext(UserContext)

    return (
        <p>{user.nickname}</p>
    )
}

이제 데이터를 사용하는 컴포넌트에서 useContext()를 사용하면 된다.
Profile 컴포넌트는 부모로부터 props를 전달받지 않아도 user 데이터를 사용할 수 있다.


Hook 사용 시 주의해야할 점

리액트에서 Hook를 사용할 땐 몇가지 규칙을 꼭 지켜서 사용해주어야한다.
Hook은 함수형 컴포넌트, 사용자정의 Hook안에서만 사용할 수 있기 때문에 반복문이나 일반 함수에선 사용할 수 없으며 호출 위치도 중요하다.


a) 컴포넌트 최상단에서 호출하기

// ❌ 사용하면 안되는 예시
if(login){
    useEffect(() => {})
}
// ✅ 옳은 예시
useEffect(() => {
    if(login){
    }
}, [login])

b) 반복문 안에서 사용하지 않기

// ❌ 사용하면 안되는 예시
users.map(() => {
    useState()
})

c) 일반 함수에서 사용하지 않기

설명

// ❌ 사용하면 안되는 예시
function test(){
    useState()
}

Hook 선택 기준

사용해야 할 상황사용할 Hook
화면에 표시되는 데이터 관리useState
렌더링 이후 작업useEffect
DOM 접근useRef
렌더링 없이 값 저장useRef
계산 결과 재사용useMemo
함수 재사용useCallback
전역 데이터 사용useContext