STUDY/[ React ]

useRef

Lim임 2025. 11. 22. 04:10

React Hooks 학습 - useEffect & useRef

1. useRef 훅

기본 개념

useRef렌더링과 무관하게 값을 유지하고, DOM 요소에 직접 접근할 수 있게 해주는 훅입니다.

문법

const 변수명 = useRef<타입>(초기값);

특징

  1. 리렌더링을 유발하지 않음 - 값이 변경되어도 컴포넌트가 다시 렌더링되지 않음
  2. 값이 유지됨 - 컴포넌트가 리렌더링되어도 값이 사라지지 않음
  3. .current 프로퍼티로 접근 - 실제 값은 .current에 저장됨

2. useRef의 두 가지 사용법

2.1 DOM 요소에 접근 (우리 코드의 경우)

const boardRef = useRef<HTMLDivElement>(null);

return <div ref={boardRef}>보드</div>;

동작 원리:

  1. useRef<HTMLDivElement>(null) - div 요소를 참조할 ref 생성
  2. ref={boardRef} - 실제 DOM div 요소와 연결
  3. boardRef.current - 실제 DOM 요소에 접근 가능

예시 - 우리 프로젝트:

export const Board = ({ gameId }: BoardProps) => {
  const boardRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (boardRef.current) {
      // boardRef.current는 실제 <div class="board"> DOM 요소
      boardRef.current.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
      });
    }
  }, []);

  return (
    <div className='board' ref={boardRef}>
      {/* 보드 내용 */}
    </div>
  );
};

타입 설명:

  • HTMLDivElement - div 요소 타입
  • HTMLInputElement - input 요소 타입
  • HTMLButtonElement - button 요소 타입
  • 등등...

2.2 값 저장 (useState와 차이점)

// useState - 값이 변경되면 리렌더링 발생
const [count, setCount] = useState(0);

// useRef - 값이 변경되어도 리렌더링 없음
const countRef = useRef(0);

비교 예시:

function Example() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);

  const handleStateClick = () => {
    setStateCount(stateCount + 1); // ✅ 리렌더링 발생!
    console.log('State 업데이트');
  };

  const handleRefClick = () => {
    refCount.current += 1; // ❌ 리렌더링 없음
    console.log('Ref 업데이트:', refCount.current);
  };

  return (
    <div>
      <p>State: {stateCount}</p> {/* 화면에 반영됨 */}
      <p>Ref: {refCount.current}</p> {/* 화면에 반영 안됨 */}
      <button onClick={handleStateClick}>State 증가</button>
      <button onClick={handleRefClick}>Ref 증가</button>
    </div>
  );
}

언제 useRef를 쓸까?

  • 이전 값을 기억해야 하지만 화면에 표시할 필요 없을 때
  • 타이머 ID 저장 (setInterval, setTimeout)
  • 스크롤 위치 추적
  • 포커스 관리

3. useEffect 훅

기본 개념

useEffect컴포넌트의 생명주기에 따라 부수 효과(side effect)를 실행하는 훅입니다.

부수 효과(Side Effect)란?

  • 데이터 가져오기 (API 호출)
  • DOM 조작 (스크롤, 포커스)
  • 타이머 설정
  • 이벤트 리스너 등록
  • 로그 출력

문법

useEffect(() => {
  // 실행할 코드

  return () => {
    // 정리(cleanup) 함수 (선택사항)
  };
}, [의존성 배열]);

4. useEffect의 실행 시점

4.1 의존성 배열 없음 - 매번 실행

useEffect(() => {
  console.log('렌더링될 때마다 실행');
});
  • 컴포넌트가 렌더링될 때마다 실행
  • 거의 사용 안 함 (성능 문제)

4.2 빈 배열 [] - 마운트 시 한 번만 (우리 코드의 경우)

useEffect(() => {
  console.log('컴포넌트가 처음 나타날 때 한 번만 실행');
}, []);

마운트(mount)란?

  • 컴포넌트가 화면에 처음 나타나는 것

언제 쓸까?

  • 초기 데이터 로딩
  • 스크롤 위치 설정 (우리 코드)
  • 초기 설정 작업

우리 프로젝트 예시:

// Board 컴포넌트가 처음 화면에 나타날 때 한 번만 실행
useEffect(() => {
  if (boardRef.current) {
    boardRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'center',
    });
  }
}, []); // 빈 배열 = 마운트 시 한 번만

실행 흐름:

1. 게임 시작 버튼 클릭
2. Board 컴포넌트 생성 (마운트)
3. JSX 렌더링 (<div ref={boardRef}>...)
4. useEffect 실행 → 스크롤 이동

4.3 특정 값 변경 시 - 의존성 배열에 값 넣기

useEffect(() => {
  console.log('count가 변경될 때마다 실행');
}, [count]);

예시:

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // userId가 변경될 때마다 사용자 정보 새로 불러오기
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, [userId]); // userId가 변경되면 실행

  return <div>{user?.name}</div>;
}

5. Cleanup 함수 (정리)

개념

컴포넌트가 언마운트(사라질 때) 또는 다음 effect 실행 전에 실행되는 함수

문법

useEffect(() => {
  // 설정
  const timer = setInterval(() => {
    console.log('1초마다 실행');
  }, 1000);

  return () => {
    // 정리
    clearInterval(timer); // 타이머 정리
  };
}, []);

언제 필요할까?

  • 타이머 정리 (clearInterval, clearTimeout)
  • 이벤트 리스너 제거
  • 구독 취소
  • 연결 해제

예시 - 이벤트 리스너:

useEffect(() => {
  const handleResize = () => {
    console.log('창 크기 변경:', window.innerWidth);
  };

  // 이벤트 리스너 등록
  window.addEventListener('resize', handleResize);

  // cleanup: 컴포넌트 사라질 때 이벤트 리스너 제거
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

왜 필요할까?

  • 메모리 누수(memory leak) 방지
  • 불필요한 작업 중단
  • 여러 번 등록되는 것 방지

6. 우리 코드 상세 분석

export const Board = ({ gameId }: BoardProps) => {
  // 1. ref 생성 - div 요소를 참조할 수 있는 변수
  const boardRef = useRef<HTMLDivElement>(null);

  // 2. Effect 설정 - Board가 마운트될 때 실행
  useEffect(() => {
    // 3. boardRef.current가 존재하는지 확인
    //    (렌더링 전에는 null이므로 체크 필요)
    if (boardRef.current) {
      // 4. DOM 메서드 호출 - 스크롤 이동
      boardRef.current.scrollIntoView({
        behavior: 'smooth', // 부드러운 애니메이션
        block: 'center', // 화면 중앙에 위치
      });
    }
  }, []); // 5. 빈 배열 - 마운트 시 한 번만 실행

  return (
    // 6. ref 연결 - boardRef와 실제 DOM 연결
    <div className='board' ref={boardRef}>
      {/* 보드 내용 */}
    </div>
  );
};

실행 순서:

[1] Board 컴포넌트 함수 실행
    ↓
[2] boardRef 생성 (초기값: { current: null })
    ↓
[3] JSX 반환 (<div ref={boardRef}>)
    ↓
[4] React가 실제 DOM 생성
    ↓
[5] boardRef.current에 DOM 요소 연결
    ↓
[6] useEffect 실행
    ↓
[7] boardRef.current 존재 확인
    ↓
[8] scrollIntoView() 호출 → 스크롤 이동!

7. scrollIntoView 메서드

문법

element.scrollIntoView(options);

옵션

{
  behavior: 'auto' | 'smooth',  // 스크롤 애니메이션
  block: 'start' | 'center' | 'end' | 'nearest', // 세로 정렬
  inline: 'start' | 'center' | 'end' | 'nearest', // 가로 정렬
}

예시:

// 즉시 이동
element.scrollIntoView();

// 부드럽게 이동, 상단 정렬
element.scrollIntoView({ behavior: 'smooth', block: 'start' });

// 부드럽게 이동, 중앙 정렬 (우리 코드)
element.scrollIntoView({ behavior: 'smooth', block: 'center' });

// 즉시 이동, 하단 정렬
element.scrollIntoView({ behavior: 'auto', block: 'end' });

8. 실전 예제

예제 1: 입력창 자동 포커스

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // 컴포넌트 마운트 시 입력창에 포커스
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder='검색...' />;
}

예제 2: 이전 값 기억하기

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

  useEffect(() => {
    // 렌더링 후 이전 값 저장
    prevCountRef.current = count;
  }, [count]);

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

예제 3: 타이머

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    // cleanup: 컴포넌트 사라질 때 타이머 정리
    return () => clearInterval(timerId);
  }, []);

  return <div>{seconds}초 경과</div>;
}

예제 4: API 호출

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 마운트 시 사용자 목록 불러오기
    fetch('/api/users')
      .then((res) => res.json())
      .then((data) => {
        setUsers(data);
        setLoading(false);
      });
  }, []); // 한 번만 실행

  if (loading) return <div>로딩 중...</div>;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

9. 주의사항

9.1 useRef는 리렌더링을 유발하지 않음

// ❌ 잘못된 사용 - 화면에 반영 안됨
const countRef = useRef(0);
countRef.current += 1;
return <div>{countRef.current}</div>; // 화면 업데이트 안됨

// ✅ 올바른 사용 - 화면에 반영됨
const [count, setCount] = useState(0);
setCount(count + 1);
return <div>{count}</div>; // 화면 업데이트됨

9.2 useEffect에서 async/await 직접 사용 불가

// ❌ 잘못된 사용
useEffect(async () => {
  const data = await fetch('/api/data');
}, []);

// ✅ 올바른 사용
useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('/api/data');
  };
  fetchData();
}, []);

9.3 의존성 배열 빠뜨리지 않기

// ❌ 경고 발생
useEffect(() => {
  console.log(count); // count 사용
}, []); // count가 의존성 배열에 없음!

// ✅ 올바른 사용
useEffect(() => {
  console.log(count);
}, [count]); // count 추가

10. 정리

특징 useRef useEffect
용도 DOM 접근, 값 저장 부수 효과 실행
리렌더링 유발 안 함 유발 안 함 (setState는 유발)
값 접근 .current -
실행 시점 즉시 렌더링 후
cleanup 없음 있음 (return)

useRef 사용 시기:

  • ✅ DOM 요소에 접근할 때
  • ✅ 리렌더링 없이 값을 유지할 때
  • ✅ 이전 값을 기억할 때

useEffect 사용 시기:

  • ✅ 컴포넌트 마운트 시 초기 작업
  • ✅ 데이터 로딩 (API 호출)
  • ✅ 특정 값 변경에 반응
  • ✅ 이벤트 리스너 등록/해제
  • ✅ 타이머 설정/정리

우리 코드에서:

  • useRef → Board div 요소에 접근
  • useEffect → Board 마운트 시 스크롤 이동
  • 빈 배열 [] → 한 번만 실행
  • scrollIntoView() → 부드럽게 중앙으로 이동

11. 연습 문제

문제 1

버튼 클릭 시 입력창에 포커스를 주는 코드를 작성하세요.

정답 보기
function FocusExample() {
  const inputRef = useRef<HTMLInputElement>(null);

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

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleFocus}>포커스</button>
    </>
  );
}

문제 2

3초 후 자동으로 메시지가 사라지는 알림을 만드세요.

정답 보기
function Notification() {
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setVisible(false);
    }, 3000);

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

  if (!visible) return null;

  return <div>알림: 3초 후 사라집니다</div>;
}

문제 3

창 크기가 변경될 때마다 현재 너비를 표시하세요.

정답 보기
function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>창 너비: {width}px</div>;
}

'STUDY > [ React ]' 카테고리의 다른 글

react new start  (0) 2025.11.22
forwardRef 사용법 - 장기 프로젝트  (0) 2025.11.22
useEffect  (0) 2025.11.22
CSS opacity와 brightness 비교 + filter 속성  (0) 2025.11.20
CSS 가상 요소 , Clip Path  (0) 2025.11.20