1. 기본 문법

useRef는 단 하나의 속성 current를 가진다.
이 속성은 언제든 읽고 쓸 수 있지만, useState와 다르게 값이 바뀐다고 해서 컴포넌트가 다시 렌더링되지는 않는다.

import { useRef } from 'react';

// 초기값과 함께 생성
const myRef = useRef(initialValue);

// 값 읽기
const value = myRef.current;

// 값 변경
myRef.current = newValue;

이처럼 current에 직접 값을 넣거나 꺼내 쓰는 방식이다.

 

 

2. useRef의 유일한 속성: current

useRef가 가진 건 단 하나, 바로 current 속성이다.

const ref = useRef(0);

// 읽기
console.log(ref.current); // 0

// 쓰기
ref.current = 10;
console.log(ref.current); // 10

// 객체도 가능
const objRef = useRef({ count: 0, name: 'test' });
objRef.current.count = 5;

여기서 중요한 점은 값을 바꿔도 리렌더링이 발생하지 않는다는 것이다.
그래서 UI에 보여줘야 하는 값은 useState를 쓰고, 컴포넌트 안에서만 관리할 값이라면 useRef를 쓰는 게 적절하다.

 

 

3. DOM 요소 참조

useRef가 가장 많이 쓰이는 용도는 DOM 요소를 직접 참조하는 것이다. 예를 들어 비디오를 재생하거나, input에 포커스를 줄 수 있다.

import { useRef, useEffect } from 'react';

function VideoPlayer() {
  const videoRef = useRef(null);
  const inputRef = useRef(null);

  useEffect(() => {
    // DOM 메서드 직접 호출
    videoRef.current.play();
    videoRef.current.volume = 0.5;

    // input에 포커스
    inputRef.current.focus();
  }, []);

  return (
    <>
      <video ref={videoRef} />
      <input ref={inputRef} />
    </>
  );
}

DOM 요소에서는 다양한 메서드를 쓸 수 있다.

 

// Video 요소
videoRef.current.play();
videoRef.current.pause();
videoRef.current.currentTime = 10;
videoRef.current.volume = 0.5;

// Input 요소
inputRef.current.focus();
inputRef.current.blur();
inputRef.current.select();
inputRef.current.value = 'text';

// 모든 요소
elementRef.current.scrollIntoView();
elementRef.current.getBoundingClientRect();
elementRef.current.classList.add('active');

 

 

4. 이전 값 저장

렌더링이 일어날 때마다 이전 값을 저장하고 싶을 때 useRef가 유용하다.
예를 들어 카운터 값이 변경될 때 이전 값도 함께 보여줄 수 있다.

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>현재: {count}</p>
      <p>이전: {prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

 

5. 타이머와 인터벌 관리

setInterval이나 setTimeout 같은 비동기 작업을 관리할 때도 useRef를 쓸 수 있다.
특히 컴포넌트가 언마운트될 때 정리하는 코드를 넣으면 메모리 누수를 막을 수 있다.

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }

    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  useEffect(() => {
    return () => {
      clearInterval(intervalRef.current);
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>시작</button>
      <button onClick={stopTimer}>정지</button>
    </div>
  );
}

 

6. 렌더링 횟수 추적

useRef는 렌더링 횟수를 기록하는 데에도 자주 쓰인다. 값이 바뀌어도 화면은 다시 그려지지 않기 때문이다.

function RenderCounter() {
  const renderCount = useRef(0);

  renderCount.current += 1;

  return <div>렌더링 횟수: {renderCount.current}</div>;
}

 

 

7. 디바운싱과 쓰로틀링

검색창 같은 곳에서 입력이 빠르게 들어올 때 API 호출을 최소화하려면 디바운싱을 적용해야 한다. 이때 useRef로 타이머를 관리하면 된다.

function SearchInput() {
  const [query, setQuery] = useState('');
  const debounceRef = useRef(null);

  const handleSearch = (value) => {
    if (debounceRef.current) {
      clearTimeout(debounceRef.current);
    }

    debounceRef.current = setTimeout(() => {
      console.log('검색:', value);
    }, 500);
  };

  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        handleSearch(e.target.value);
      }}
    />
  );
}

 

 

8. 이벤트 상태 플래그

특정 이벤트를 한 번만 실행하거나, 일정 간격으로 실행되도록 하고 싶을 때도 useRef가 편하다.

function VideoPlayer() {
  const hasCompletedRef = useRef(false);
  const lastSaveTimeRef = useRef(0);

  const handleComplete = () => {
    if (!hasCompletedRef.current) {
      hasCompletedRef.current = true;
      console.log('완료 처리');
    }
  };

  const handleSave = () => {
    const now = Date.now();
    if (now - lastSaveTimeRef.current > 5000) {
      lastSaveTimeRef.current = now;
      console.log('저장');
    }
  };

  return <div>Video Player</div>;
}

 

9. useRef와 useState의 차이

많은 주니어 개발자가 헷갈려 하는 부분이다.
언제 useRef를 쓰고, 언제 useState를 써야 하는지 정리하면 아래와 같다.

// useRef
const ref = useRef(value);
ref.current = newValue;  // 값 변경 → 리렌더링 없음

// useState
const [state, setState] = useState(value);
setState(newValue);  // 값 변경 → 리렌더링 발생
  • UI에 보여야 하는 값 → useState
  • 내부적으로만 관리하는 값 → useRef

 

 

10. 정리와 주의사항

마지막으로, useRef는 다음 상황에서 꼭 정리(clean-up)를 해주는 것이 좋다.

useEffect(() => {
  return () => {
    clearTimeout(timerRef.current);
  };
}, []);

그리고 중요한 점 하나.
useRef 값은 바뀌어도 컴포넌트는 다시 렌더링되지 않는다.
따라서 화면에 보여줘야 하는 값이라면 반드시 useState를 써야 한다.

 

 
// 정렬된 리뷰 계산
  const sortedReviews = useMemo(() => {
    const reviewsCopy = [...reviews];
    
    switch (sortBy) {
      case 'highest-rated':
        return reviewsCopy.sort((a, b) => b.rating - a.rating);
      case 'lowest-rated':
        return reviewsCopy.sort((a, b) => a.rating - b.rating);
      case 'most-recent':
        return reviewsCopy.sort((a, b) => new Date(b.date) - new Date(a.date));
      default:
        return reviewsCopy;
    }
  }, [sortBy]);

  // 현재 페이지의 리뷰 계산
  const currentReviews = useMemo(() => {
    const startIndex = (currentPage - 1) * reviewsPerPage;
    const endIndex = startIndex + reviewsPerPage;
    return sortedReviews.slice(startIndex, endIndex);
  }, [sortedReviews, currentPage]);

  // 총 페이지 수 계산
  const totalPages = Math.ceil(sortedReviews.length / reviewsPerPage);

  // 정렬이 변경되면 첫 번째 페이지로 이동
  useEffect(() => {
    setCurrentPage(1);
  }, [sortBy]);

 

 

현재 리뷰 계산을 위한 함수를 작성중이다. 

 

이때 의존성 배열이 2개일 경우,

하나만 바뀌어도 작동하는가 or 두개 모두 바뀌어야 작동하는가?

 

 

 

결론은 하나만 바뀌어도 작동한다.

React Hook이란?

React Hook은 함수형 컴포넌트에서 상태 관리와 생명주기 기능을 사용할 수 있게 해주는 함수들입니다. React 16.8부터 도입되었습니다.

자주 사용하는 Hook들

1. useState - 상태 관리

가장 기본적이고 자주 사용하는 Hook입니다.

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  const [user, setUser] = useState({ name: '', age: 0 });

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={() => setCount(prev => prev - 1)}>감소</button>
      
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="이름 입력"
      />
    </div>
  );
}

주요 특징:

  • 초기값을 설정할 수 있습니다
  • 배열 구조분해를 사용합니다: [상태, 상태변경함수]
  • 상태 변경 함수는 비동기적으로 작동합니다
  • 함수형 업데이트를 사용할 수 있습니다: setCount(prev => prev + 1)

 

 

 

 

 

2. useEffect - 사이드 이펙트 처리

컴포넌트의 생명주기와 관련된 작업을 처리합니다.

import React, { useState, useEffect } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // 컴포넌트가 마운트될 때만 실행
  useEffect(() => {
    fetchUserData();
  }, []);

  // user가 변경될 때마다 실행
  useEffect(() => {
    if (user) {
      console.log('사용자 정보가 업데이트됨:', user);
    }
  }, [user]);

  // 컴포넌트가 언마운트될 때 정리 작업
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('타이머 실행');
    }, 1000);

    return () => {
      clearInterval(timer); // 정리 함수
    };
  }, []);

  const fetchUserData = async () => {
    try {
      const response = await fetch('/api/user');
      const data = await response.json();
      setUser(data);
    } catch (error) {
      console.error('에러:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div>로딩 중...</div>;
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  );
}

useEffect의 의존성 배열:

  • []: 컴포넌트 마운트 시에만 실행
  • [변수]: 해당 변수가 변경될 때마다 실행
  • undefined: 모든 렌더링 후 실행
  • 정리 함수를 반환하면 컴포넌트 언마운트 시 실행

 

 

 

 

 

3. useContext - 전역 상태 관리

컴포넌트 트리 전체에서 데이터를 공유할 때 사용합니다.

import React, { createContext, useContext, useState } from 'react';

// Context 생성
const ThemeContext = createContext();
const UserContext = createContext();

// Provider 컴포넌트
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: '홍길동', role: 'user' });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Header />
        <Main />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Context 사용
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <h1>안녕하세요, {user.name}님!</h1>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        테마 변경
      </button>
    </header>
  );
}

 

 

 

 

 

 

4. useRef - DOM 요소 참조

DOM 요소에 직접 접근하거나 값을 저장할 때 사용합니다.

import React, { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null);
  const countRef = useRef(0);

  useEffect(() => {
    // 컴포넌트 마운트 시 자동으로 포커스
    inputRef.current.focus();
  }, []);

  const handleClick = () => {
    inputRef.current.focus();
    countRef.current += 1;
    console.log('클릭 횟수:', countRef.current);
  };

  return (
    <div>
      <input ref={inputRef} placeholder="여기에 입력하세요" />
      <button onClick={handleClick}>포커스 이동</button>
    </div>
  );
}

 

 

 

 

 

 

 

5. useMemo - 메모이제이션

계산 비용이 큰 값을 메모이제이션합니다.

import React, { useState, useMemo } from 'react';

function ExpensiveCalculation() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // count가 변경될 때만 재계산
  const expensiveValue = useMemo(() => {
    console.log('복잡한 계산 실행');
    let result = 0;
    for (let i = 0; i < count * 1000000; i++) {
      result += i;
    }
    return result;
  }, [count]);

  return (
    <div>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
        placeholder="텍스트 입력"
      />
      <button onClick={() => setCount(count + 1)}>
        카운트 증가: {count}
      </button>
      <p>계산 결과: {expensiveValue}</p>
    </div>
  );
}

6. useCallback - 함수 메모이제이션

함수를 메모이제이션하여 불필요한 리렌더링을 방지합니다.

import React, { useState, useCallback } from 'react';

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // count가 변경될 때만 함수가 재생성됨
  const handleIncrement = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);

  const handleReset = useCallback(() => {
    setCount(0);
  }, []);

  return (
    <div>
      <input 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
      />
      <p>카운트: {count}</p>
      <ChildComponent onIncrement={handleIncrement} onReset={handleReset} />
    </div>
  );
}

function ChildComponent({ onIncrement, onReset }) {
  console.log('ChildComponent 렌더링');
  
  return (
    <div>
      <button onClick={onIncrement}>증가</button>
      <button onClick={onReset}>리셋</button>
    </div>
  );
}

 

 

Hook 사용 규칙

  1. 최상위에서만 Hook 호출: 조건문, 반복문, 중첩 함수 내부에서 호출하면 안 됩니다.
  2. React 함수에서만 Hook 호출: 일반 JavaScript 함수에서는 호출하면 안 됩니다.
  3. Hook의 순서는 항상 동일해야 함: 조건부로 Hook을 호출하면 안 됩니다.

이러한 기본 Hook들을 잘 활용하면 React 함수형 컴포넌트에서 강력하고 효율적인 애플리케이션을 만들 수 있습니다. 추가로 궁금한 Hook이나 더 자세한 설명이 필요하시면 말씀해 주세요!

React 커스텀 훅 작성 시 주의사항

커스텀 훅이란?

로직 재사용을 위한 함수.

use로 시작하며, 공식 훅(useState, useEffect 등)을 조합해 작성함.


네이밍 규칙

  • 반드시 use로 시작해야 함
  • 예: useAuth, useMyData

훅 호출 위치

  • 컴포넌트나 다른 훅의 최상단에서만 호출해야 함
  • 조건문이나 반복문 내부에서 호출하면 안 됨

의존성 배열 관리

  • useEffect, useCallback 사용 시 의존성 배열 정확히 설정해야 함
  • 누락 또는 잘못 작성 시 버그 발생 가능

단일 책임 원칙

  • 하나의 훅은 하나의 목적만 가져야 함
  • 여러 기능을 섞지 말아야 유지보수가 쉬움

성능 최적화

  • useMemo, useCallback으로 불필요한 렌더링 방지 가능
  • 상태 변화가 잦은 훅일수록 최적화 중요

테스트 용이성

  • 커스텀 훅은 함수형 로직이므로 테스트하기 쉬워야 함
  • 외부 의존성을 줄이고 예측 가능한 동작을 해야 함

 

 

 

React 라이프사이클 정리

라이프사이클이란?

컴포넌트 생성 → 업데이트 → 제거의 흐름을 의미

상태 관리와 외부 자원 해제에 중요


라이프사이클 단계

단계 시점 주로 하는 일

마운트 컴포넌트 최초 렌더링 시 초기 데이터 로딩, 구독 설정 등
업데이트 props 또는 state 변경 시 값 반응 처리, API 재요청 등
언마운트 컴포넌트 제거 시 타이머 해제, 이벤트 제거 등

라이프사이클 예시 코드

jsx
복사편집
useEffect(() => {
  console.log('실행됨'); // 마운트 또는 업데이트 시

  return () => {
    console.log('정리됨'); // 언마운트 또는 다음 업데이트 전
  };
}, [someValue]);


의존성 배열 주의사항

  • 너무 많은 값 넣으면 성능 저하
  • 꼭 필요한 값만 넣는 것이 핵심
  • 이벤트 리스너, 타이머 등은 반드시 정리 코드 작성해야 함

+ Recent posts