-
useState, useRef, useEffect카테고리 없음 2025. 11. 11. 09:49
✅ 1. useState — 화면에 보여지는 값을 저장
✅ 개념
- 화면을 다시 그리게 만드는 상태값을 저장
- 값이 바뀌면 컴포넌트가 리렌더링됨
✅ 비유
"게시판 글 수", "내가 입력한 텍스트", "다크모드 on/off" 같은 화면에 영향을 주는 값
✅ 사용 예시
import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); // count = 상태 값, setCount = 수정 함수 return ( <div> <p>현재 카운트: {count}</p> <button onClick={() => setCount(count + 1)}> +1 </button> </div> ); }✅ count 값이 바뀌면 화면이 다시 렌더링됨
✅ 2. useRef — 눈에 안 띄는 값을 저장
✅ 개념
- 값이 바뀌어도 렌더링이 일어나지 않는 저장공간
- 주로 DOM 요소 선택, 변하지 않는 값 저장에 사용
✅ 비유
화면에 안 보이게 몰래 저장하는 주머니
예) "버튼이 몇 번 클릭됐는지", "input DOM을 가져오기"✅ DOM 접근 예시
import { useRef } from "react"; function InputFocus() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); // DOM에 직접 접근 }; return ( <div> <input ref={inputRef} /> <button onClick={focusInput}>포커스 주기</button> </div> ); }✅ DOM 제어 필요할 때 useRef
✅ 3. useEffect — 렌더링 이후 실행되는 함수
✅ 개념
- 화면이 렌더링된 후 실행되는 “사후 처리”용
- API 호출, 이벤트 등록, 타이머, 로컬스토리지 저장 등에 사용
✅ 비유
“컴포넌트 만들어지고 난 뒤 실행해야 하는 일”
✅ 기본 예시 — 컴포넌트 처음 실행될 때 한 번만
import { useEffect } from "react"; useEffect(() => { console.log("처음 화면에 나타났습니다!"); }, []); // 빈 배열이면 최초 1번만 실행
✅ 값이 바뀔 때마다 실행
const [count, setCount] = useState(0); useEffect(() => { console.log(`count가 변경됨 : ${count}`); }, [count]); // count가 바뀔 때 실행
✅ 언마운트 시 정리(clean-up)
useEffect(() => { const timer = setInterval(() => console.log("타이머"), 1000); return () => { clearInterval(timer); // 컴포넌트 사라질 때 정리 }; }, []);
✅ 한 줄 정리
Hook언제 씀?값이 바뀌면 리렌더링?주 사용 목적useState 화면에 영향을 주는 상태 ✅ Yes UI 업데이트 useRef 렌더링 필요 없는 값, DOM 선택 ❌ No DOM 제어, 임시 저장 useEffect 렌더링 후 실행해야 하는 작업 - API, 이벤트, 타이머 좋아, 요청한 3가지를 실무 감각으로 딱 이해되게 정리해줄게.
✅ 1) useEffect 무한 렌더링 나는 경우
✅ 왜 생기나?
useEffect 안에서 state를 업데이트(setState) 하는데,
`dependency 배열([])에 그 state를 넣었을 때 계속 반복 호출되기 때문.❌ 잘못된 예 (무한 렌더링)
const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); }, [count]);동작
- 처음 렌더링 → count = 0
- useEffect 실행 → setCount(1) → 렌더링
- count가 바뀜 → useEffect 다시 실행 → 또 setCount
- 계속 반복 → 무한 렌더링 ❌
✅ 해결 방법
✅ 1. 의존성을 없애고 한 번만 실행하고 싶다면 → []
useEffect(() => { setCount(c => c + 1); }, []);✅ 2. 조건을 걸어서 변경 관리
useEffect(() => { if (count < 5) { setCount(count + 1); } }, [count]);✅ 3. state 업데이트는 연산 결과만 effect에서 처리 (비추천되는 setState 남발 제거)
✅ 2) useState vs useRef — 어떤 상황에서 골라 쓰는지?
비교 요소useStateuseRef값 변경 시 리렌더링 ✅ Yes ❌ No 언제 사용? UI에 영향을 주는 값 UI와 관계없는 값 예시 입력한 글자, 테마 모드, 모달 열림/닫힘 DOM 선택, 이전 값 저장, 타이머 id ✅ 쉽게 구분하는 기준
이런 상황이면사용값이 바뀌면 화면이 바뀌어야 한다 useState 값이 바뀌어도 화면이 바뀔 필요가 없다 useRef DOM 요소에 접근해야 한다 useRef setState 쓰면 리렌더링이 너무 많이 일어난다 useRef 예시
✅ 상태는 바뀌지만 화면에서 안 보여야 할 값
(예: API 호출 횟수, 버튼 클릭 누적, 스크롤 위치 기록)const clickCount = useRef(0); function handleClick() { clickCount.current += 1; // UI에 안 보임 }✅ DOM 조작
const inputRef = useRef(null); <input ref={inputRef} />✅ 화면에 보여줄 값
const [name, setName] = useState('');
✅ 3) 실무에서 가장 많이 쓰는 패턴 정리
✅ API 호출 + 로딩/에러 상태 관리
const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { const res = await fetch('/api/user'); const result = await res.json(); setData(result); setLoading(false); }; fetchData(); }, []);✅ 스크롤/resize 이벤트 등록 & cleanup
useEffect(() => { const onScroll = () => console.log("scrolling..."); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []);✅ 타이머 + component unmount 시 정리
useEffect(() => { const timer = setInterval(() => console.log("tick"), 1000); return () => clearInterval(timer); }, []);✅ useRef로 이전 값 기억하기
const prevValue = useRef(); useEffect(() => { prevValue.current = value; }, [value]);✅ input DOM 제어
const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); };✅ State 대신 Ref로 렌더링 줄이기
(예: 사용자가 키 입력할 때마다 렌더링 막고 싶을 때)const typingValue = useRef(""); function onChange(e) { typingValue.current = e.target.value; }
✅ 요약 한 번에 끝내기
✅ 무한 렌더링 생길 때
→ useEffect 안에서 setState + dependency에 그 state 넣었을 때✅ useState vs useRef
- 화면 업데이트 필요하면 → useState
- 업데이트 필요없고 값만 유지하거나 DOM 접근 → useRef
✅ 실무에서 가장 많이 쓰는 패턴
- API 호출
- 이벤트 등록/정리
- 타이머 관리
- DOM 접근
- 렌더링 줄이기 용도로 useRef 사용
✅ 실제 예 — 3가지 함께 쓰기
import { useState, useEffect, useRef } from "react"; function Example() { const [count, setCount] = useState(0); const renderCount = useRef(1); useEffect(() => { renderCount.current += 1; console.log(`렌더링 횟수: ${renderCount.current}`); }); return ( <div> <p>count: {count}</p> <button onClick={() => setCount(count + 1)}> +1 </button> </div> ); }✅ count가 바뀌면 화면 다시 그림 (useState)
✅ 렌더링 횟수는 ref로 저장되며 화면엔 영향 없음
✅ 렌더 후마다 Effect 실행됨
필요하면 더 확장해서 이런 것도 알려줄 수 있어:
✅ useEffect 무한 렌더링 나는 경우
✅ useState·useRef 어떤 상황에서 각각 골라 쓰는지
✅ 실무에서 제일 많이 쓰는 패턴 정리✅ 그럼 코드에서 "언마운트 시"는 어디야?
바로 useEffect의 return() 내부 👇
useEffect(() => { const timer = setInterval(() => console.log("타이머"), 1000); return () => { clearInterval(timer); // ✅ 여기가 언마운트 시 실행되는 코드 }; }, []);✅ 정리하자면:
- useEffect의 return에 담긴 함수 = cleanup(정리용 함수)
- 이 함수는 컴포넌트가 사라질 때 자동 실행
✅ 실제 예로 이해하기
예를 들어 페이지 이동할 때:
✅ Home.jsx → Timer.jsx로 갔다가
❌ 다시 Home.jsx로 돌아오면➡ Timer 컴포넌트는 사라짐(unmount)
➡ 타이머 계속 돌면 메모리 낭비 / 콘솔 도배
➡ 그래서 return 안에서 제거해야 함
✅ 비유
- 타이머 켰으면 → 나갈 때 끄고 나가는 게 정상
- 이벤트리스너 등록했으면 → 떠날 때 제거
- WebSocket 연결했으면 → 사라질 때 끊기
이 행동을 자동으로 실행시켜주는 위치 = return() 내부
✅ 추가 예시 (스크롤 이벤트 정리)
useEffect(() => { const handleScroll = () => console.log("스크롤!"); window.addEventListener("scroll", handleScroll); return () => { // ✅ 컴포넌트 사라질 때 이벤트 제거 window.removeEventListener("scroll", handleScroll); }; }, []);
혹시,
- 언마운트 상황이 실제로 언제 발생하는지?
- useEffect cleanup이 없을 때 어떤 문제가 생기는지?
- 렌더될 때마다 cleanup 실행되는 케이스?
이런 것도 궁금하면 더 자세히 예제로 알려줄게!
✅ 0) .current는 뭐야?
✅ .current는 useRef가 만들어주는 일반 객체의 프로퍼티(값 저장공간) 이다.
const ref = useRef(0); console.log(ref); // { current: 0 }- 특별한 메서드(method)가 아님 ❌
- 그냥 { current: 원하는 값 } 형태의 객체
- 우리가 값을 마음대로 바꿀 수 있는 저장칸
ref.current = 10;✔ useRef는 항상 이렇게 생긴 하나의 객체를 반환할 뿐(불변 아님)
✔ 이 객체는 컴포넌트가 리렌더링될 때도 사라지지 않음
✅ 1) "setState 전에 저장하면 되지 않나?" 왜 안될까?
보통 이런 생각이 든다:
setPrev(value); // 이전 값 저장 setValue(newValue);겉으로 보면 맞는 것 같지만…
❌ 문제: setState는 비동기
즉, 호출했다고 바로 값이 바뀌는 게 아니라
React가 "렌더링 스케줄"을 잡아서 나중에 변경함
❌ 잘못된 코드 예
const [value, setValue] = useState(0); const [prev, setPrev] = useState(0); function handleClick() { setPrev(value); setValue(value + 1); console.log("이전값: ", prev); // ❌ 기대한 값이 안 나올 수 있음 }왜 문제?
setPrev도, setValue도 렌더링이 일어나야 값이 바뀜
즉:- 버튼 클릭
- setPrev(value) 실행
- setValue(value + 1) 실행
- 아직 둘 다 반영되지 않음
- 그대로 console.log 실행 → 최신값이 아님
✅ 결론: state는 즉시 바뀌지 않는다
➡ "지금" 정확하게 이전 값을 저장하고 싶다면 state로는 못함
➡ 그래서 ref를 쓰는 것
✅ 2) "그럼 useState로 이전 값 저장하면 뭐가 문제?"
✅ 리렌더링이 계속 발생
const [prev, setPrev] = useState(0);setPrev(…)를 호출하면 → 리렌더링 발생
리렌더링 → effect 실행 → 또 setPrev → 또 리렌더
→ 무한 렌더링 가능성 ↑✅ 성능 낭비
“이전 값”은 화면에 보여줄 필요가 없는데
state로 두면 불필요한 렌더링이 생김✅ ref는 값이 바뀌어도 화면을 다시 그리지 않음
→ 딱 기억만 하는 용도로 적합
✅ 3) 비동기 업데이트 상황에서 왜 ref가 더 안정적?
state는 ❗비동기 + 묶어서(batch) 처리됨
즉 하나씩 바로바로 바뀌는 게 아니라
React가 “묶어서 한 번에” 렌더링할 수 있음➡ 그렇다면 이런 상황
setValue(value + 1); setValue(value + 1); setValue(value + 1);기대값? 3 증가
하지만 실제론 1만 증가할 때가 있음그래서 안전하게 하려면:
setValue(v => v + 1);즉 콜백 패턴 써야 함
✅ 그런데 ref는?
- 그냥 객체일 뿐
- 비동기, batch 처리 영향을 받지 않음
- 그 순간 즉시 바뀜
ref.current = ref.current + 1; // 즉시 반영➡ “지금 당장, 확정된 값”을 저장하는데 더 정확함
➡ 렌더링 없이도 값이 유지됨그래서 이전 값, 누적 값, 이벤트 횟수, 타이머 ID 같은 건 ref가 정답
✅ 정리 한 컷
비교useStateuseRef값 바뀌면 렌더링됨 ✅ Yes ❌ No 비동기 업데이트? ✅ 영향을 받음 ❌ 안 받음 batch 처리 적용? ✅ Yes ❌ No 즉시 값 저장 가능? ❌ 때때로 예상과 다름 ✅ 즉시 저장 이전 값 저장에 적합? ❌ 비효율적 ✅ 최적
✅ 이해 완전하게 되는 실제 예
function Example() { const [value, setValue] = useState(0); const prev = useRef(0); const increase = () => { prev.current = value; // ✅ "지금"의 값을 정확히 기억 setValue(value + 1); // ✅ state는 나중에 반영 }; return ( <> <p>현재값: {value}</p> <p>이전값: {prev.current}</p> <button onClick={increase}>+1</button> </> ); }✅ 매번 클릭할 때마다 이전 값이 정확하게 유지됨
✅ 화면에 보여줘야 하는 값(value)은 state
✅ 화면에 안 보여도 기억해야 하는 값(prev)은 ref"이전값을 어디에 저장해야 맞는가?"
→ 정답이 하나가 아니라 상황에 따라 다르다.
아래 기준만 알면 항상 정확하게 판단할 수 있어.
✅ 1) 컴포넌트 내부에서만 필요하다 → 자식(useRef 또는 useState)
✅ 가장 흔한 경우
- 이전 값이 그 컴포넌트 안에서만 쓰임
- 다른 컴포넌트가 알 필요 없음
- 화면에 표시하거나 비교만 하면 됨
➡ 이럴 땐 자식 내부에 저장 (useRef)
function Product({ price }) { const prevPrice = useRef(price); useEffect(() => { console.log(prevPrice.current, price); prevPrice.current = price; }, [price]); return <div>{price}</div>; }✔ 가장 간단
✔ 렌더링을 일으키지 않음
✔ 다른 곳에서 접근 불필요✅ 이 경우 = 자식 안에 저장이 정답
✅ 2) 자식에서 계산하고, 부모가 알아야 한다 → 부모 state
언제?
- 부모가 이전값을 기준으로 다른 UI를 바꾸고 싶다
- 다른 컴포넌트도 이전값이 필요하다
➡ 이럴 땐 부모 state에 저장
function App() { const [price, setPrice] = useState(10000); const [prevPrice, setPrevPrice] = useState(10000); const increase = () => { setPrevPrice(price); setPrice(price + 5000); }; return ( <> <Product price=728x90