JAN's History
React 학습하기 - 탈출구 본문
https://ko.react.dev/learn/escape-hatches 를 보고 정리한 블로그입니다.
1. Ref로 값 참조하기
컴포넌트 ref를 어떻게 추가할까?
useRef 는 React에서 제공하는 훅이며, 다음처럼 사용한다.
import { useRef } from "react";
const ref = useRef(initialValue);
|
- initialValue는 ref.current의 초기값
- ref 객체는 { current: initialValue } 형태이며, ref.current를 통해 값을 읽고 쓸 수 있다.
ref의 값이 어떻게 업데이트 될까?
ref.current 는 일반 객체처럼 직접 수정할 수 있다.
ref.current = 5;
|
- 변경 시 리렌더링 X
- 렌더 시 값 유지
useState vs useRef
값 변경 시 리렌더 | 됨 | 안 됨 |
렌더링 간 값 유지 | 됨 | 됨 |
읽기/쓰기 방식 | setter 필요 | .current 직접 접근 |
주요 용도 | UI 갱신, 사용자 입력 반영 | DOM 조작, interval ID, 이전 값 저장 등 |
언제 useRef 를 써야 할까?
- DOM 요소 직접 조작
- 이전 값 기억
- setInterval, setTimeout ID 저장
- 리렌더링 없이 참조만 하고 싶은 값 저장
//클릭 수 세기 (UI표현 X)
//화면은 바뀌지 않지만, 값은 계속 누적된다.
function Counter() {
const countRef = useRef(0);
function handleClick() {
countRef.current += 1;
console.log(`Clicked ${countRef.current} times`);
}
return <button onClick={handleClick}>Click me</button>;
}
|
정리
- 리렌더링 필요 없는 값 -> useRef
- 렌더링 필요 -> useState
- ref.current 는 즉시 읽고 쓰기 가능하지만, UI에는 영향 없음
- 탈출구 의미 : ref는 React의 감시망을 피해서 몰래 데이터를 수정할 수 있기 때문에 자주 사용하면 좋지 않다. (= 상태(state)가 바뀌면 → React가 알아차림 → DOM 업데이트를 우회할 수 있음)
2. Ref로 DOM 조작하기
ref 어트리뷰트로 React가 관리하는 DOM 노드에 접근하는 법
- ref를 DOM에 넣으면 ref.current에 해당 DOM이 저장된다.
- 이벤트 핸들러나 effect 내에 DOM API를 호출할 수 있다.
- 초기에는 ref.current === null
- React가 렌더 후 자동으로 DOM을 넣어줌
const inputRef = useRef(null);
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
|
ref JSX 어트리뷰트와 useRef Hook의 관련성
- useRef()는 { current : null } 형태의 객체를 반환
- JSX에서 ref={myRef}로 넘기면 React가 알아서 DOM과 연결
다른 컴포넌트의 DOM 노드에 접근하는 방법
<input ref={myRef} /> // 가능
<MyInput ref={myRef} /> // 기본적으로 안 됨
|
- 직접 만든 컴포넌트는 기본적으로 ref를 받지 않기 때문에 전달하려면 forwardRef()를 사용해야한다.
- <MyInput />처럼 사용자 정의 컴포넌트에는 ref를 자동으로 넘기지 않기 때문에, 접근이 필요할 경우 forwardRef로 명시적으로 허용해줘야한다.
const MyInput = React.forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
|
- 첫 번째 인자는 일반 props, 두 번재 인자 ref는 부모가 전달한 ref 객체
React가 관리하는 DOM을 수정해도 안전한 경우
ref.current.remove(); // X 직접 DOM을 제거하면 충돌 가능
ref.current.scrollIntoView(); // O 비 파괴적
ref.current.focus(); // O 비 파괴적
|
- React가 갱신하지 않는 영역이라면 DOM을 조작해도 괜찮다.
정리
- Ref는 보통 DOM을 참조하기 위해 사용된다.
- 많은 경우 ref는 포커싱, 스크롤, DOM 요소 크기 혹은 위치 측정 등 비 파괴적인 행동을 위해 사용된다.
- React가 관리하는 DOM 노드를 바꾸면 안된다. (remove)
- React에 <div ref={myRef}>와 같이 작성하면 myRef.current에 값이 들어간다.
- 컴포넌트는 기본적으로 DOM 노드를 노출하지 않기 때문에 forwardRef의 두번째 인자에 ref를 전달하는 것으로 선택적으로 노출할 수 있다.
3. Effect로 동기화하기
Effect가 무엇일까
- Effect는 React 컴포넌트의 렌더링 결과를 기반으로 하는 부수효과를 처리하기 위한 훅이다.
- useEffect는 렌더링이 끝난 후 DOM이 그려진 뒤에 실행된다.
- API 호출, 브라우저 API 사용 등에 사용된다.
- 이벤트 핸들러는 사용자 동작에 의해 실행되는 반면, Effect는 렌더링 자체에 의해 실행된다.
Effect가 이벤트와 다른점
실행 시점 | 사용자의 상호작용(클릭 등) 시 | 렌더링 직후 (커밋 이후) |
목적 | 상태 변경, 네트워크 요청 등 직접 처리 | 외부 시스템과의 동기화 |
예시 | onClick={() => setState(x)} | useEffect(() => {...}, [x]) |
Effect 작성법
//1. Effect 선언
useEffect(() => {
// 실행할 코드
});
//2. 의존성 배열 지정
// 언제 다시 실행할지 지정
// [] : 최초 1회 (마운트 시)
// [a, b] : a 또는 b가 바뀔 때만 실행
// 생략 시 : 모든 렌더링 마다 실행
//3. 클린업 함수
//반환된 함수는 다음 Effect 실행 전이나 컴포넌트 언마운트 시 호출됨
useEffect(() => {
const timer = setTimeout(() => console.log("hi"), 1000);
return () => clearTimeout(timer); // 정리
}, []);
|
불필요한 Effect 재실행 건너뛰기
- useEffect(() => {...}, [x]) 의 두 번째 인자로 의존성 배열을 반드시 선언해야 React가 변경된 값만 감지하고 불필요한 실행을 피할 수 있다.
- 의존성 배열 생략 시 매 렌더링마다 실행됨 -> 성능 이슈 및 예상치 못한 동작 유발 가능
개발 중 Effect가 두 번 실행되는 이유 (strick Mode)
- React의 Strict Mode는 Effect가 제대로 cleanup되는지 테스트하기 위해 개발 환경에서 컴포넌트를 두 번 마운트한다.
- 이로 인해 useEffect()는 두 번 실행되며, 버그가 있으면 드러나게 된다.
- 배포 환경에선 발생하지 않는다!
외부 API와 동기화 | useEffect(() => fetchData(), [dependency]) |
이벤트 등록 | addEventListener, 그리고 cleanup으로 removeEventListener |
타이머, 애니메이션 | setTimeout, requestAnimationFrame 등, cleanup 필요 |
정리
- Effect는 렌더링 자체에 의해 발생
- Effect를 사용하면 외부 시스템 (네트워크, 타사 API)과 동기화가 가능하다.
- 기본적으로 Effect는 모든 렌더링 후에 실행
- 의존성 배열은 "선택"할 수 없고, Effect 내부 코드에 의해 결정된다.
- 빈 의존성 배열 []은 "마운팅"을 의미한다.
- Strick Mode에서 React는 컴포넌트를 두번 마운트한다 (개발 환경에서만)
- Effect가 다시 마운트로 인해 중단된 경우 클린업 함수를 구현해야한다.
- React는 Effect가 다음에 실행되기 전 정리 함수를 호출하며, 마운트 해제 중에도 호출한다.
4. Effect가 필요하지 않을 수도 있다.
컴포넌트에서 불필요한 Effect를 제거하는 이유와 방법
- Effect는 외부 시스템과 동기화할 때만 필요하다. (API호출, 브라우저 API조작..)
- 단순한 데이터 변환이나 렌더링 로직, 이벤트 응답은 Effect 없이 처리해야 더 간결하고 빠르고, 예측 가능한 코드가 된다.
- 불필요한 Effect는 불필요한 리렌더링, 복잡성 증가, 버그를 초래한다.
Effect 없이 값비싼 계산을 캐싱하는 방법
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
|
- 렌더링 중 계산 비용이 비싸면 useMemo로 감싸서 캐싱
- useMemo는 의존성이 변경될 때만 계산 수행 -> 성능 최적화 가능
Effect 없이 컴포넌트 state 초기화 및 조정하는 방법
- props가 변경될 때 state를 초기화하려고 Effect를 쓰는 것은 비효율적이다.
- 대신 key props를 이용해 컴포넌트를 다시 마운트하게 하거나,
- 또는 렌더링 도중 조건을 보고 state를 직접 초기화하는 것이 좋다.
//key로 강제 마운트
<Profile userId={userId} key={userId} />
//렌더 중 상태 조정
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
|
이벤트 핸들러 간 로직을 공유하는 방법
- 이벤트로 발생하는 동작은 Effect가 아니라 이벤트 핸들러 내부에서 처리해야한다.
- 공통 로직은 핸들러 내부에서 공통 함수로 추출해 재사용
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name}`);
}
function handleBuyClick() {
buyProduct();
}
|
이벤트 핸들러로 이동해야 하는 로직
- 사용자의 직접적인 상호작용(클릭 등)에 의한 동작은 무조건 핸들러 안에서 처리해야한다.
- Effect에서 처리할 경우 예기치 않는 중복 실행이나 버그 발생 가능
//잘못된 방식
useEffect(() => {
if (product.isInCart) {
showNotification(...);
}
}, [product]);
//올바른 방식
function handleClick() {
addToCart(product);
showNotification(...);
}
|
부모 컴포넌트에 변경 사항을 알리는 방법
- 자식 컴포넌트의 state가 바뀔 때 Effect를 통해 부모에게 알리는것은 좋지 않다.
- 대신, 자식의 이벤트 핸들러에서 즉시 부모 콜백(onChange 등)을 호출하도록 해야한다.
function updateToggle(nextState) {
setIsOn(nextState);
onChange(nextState);
}
|
데이터 가져오기
useEffect(() => {
// XXX 정리 로직 없이 가져오기
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
|
정리
- 렌더링 중 계산을 할 수 있다면 Effect가 필요하지 않다.
- 비용이 많이 드는 계산을 캐싱하려면 useEffect 대신 useMemo를 추가하자
- 전체 컴포넌트 트리의 state를 초기화하려면 다른 key를 전달하자
- 컴포넌트가 표시되어 실행되는 코드는 Effect에 있어야 하고 나머지는 이벤트에 있어야 한다.
- 여러 state를 업데이트 해야하는 경우 단일 이벤트 중에 수행하는 것이 좋다.
- 다른 컴포넌트의 state를 동기화할 때마다 state를 끌어올리기를 고려하자
- Effect로 데이터를 가져올 수 있지만 경쟁 조건을 피하기 위해 정리를 구현해야한다.
5. 반응형 effects의 생명주기
effect의 생명주기가 컴포넌트의 생명주기와 다른 점
- 컴포넌트: 마운트 -> 업데이트 -> 언마운트.
- effect: 단순히 동기화 시작과 중지(cleanup)만 한다.
- 하나의 컴포넌트가 마운트된 상태에서도, props나 state가 바뀌면 effect는 여러 번 실행되고 정리될 수 있다.
- effect는 컴포넌트의 생명주기를 따라가지 않고, 의존성 값의 변화에 따라 스스로 시작/중지된다.
각 effect를 개별적으로 생각하는 방법
- 하나의 effect는 하나의 외부 동기화 프로세스를 담당해야한다.
- ex) 채팅 서버 연결 effect 와 방문 로그 effect는 분리해야함 -> 서로 다른 목적이기 때문'
- But 혼잡한 effect는 복잡성과 버그 발생을 높인다.
- useEffect(() => { /* 연결 */ return () => { /* 해제 */ } }, [roomId])처럼 명확하게 시작과 정리를 정의.
effect를 다시 동기화해야하는 시기와 그 이유
- effect의 의존성이 바뀌면 다시 실행되며 이전 effect는 cleanup된다
- ex) roomId가 바뀌면 기존 채팅방과의 연결은 끊고 새 채팅방과 연결해야한다.
- React는 의존성 배열의 값이 바뀐 경우에만 effect를 다시 실행한다.
effect의 의존성이 결정되는 방법
- effect 내부에서 사용하는 모든 반응형 값(state, props, context, 렌더링 중 계산된 값)을 의존성 배열에 포함
- 포함되지 않으면 버그 발생 및 React 린트 에러 발생 가능
- useEffect(() => { log(roomId) }, [roomId])처럼 사용한 값은 무조건 넣어야 한다.
값이 유동적이라는 의미
- 컴포넌트 내부에서 선언된 모든 값은 렌더링마다 달라질 수 있기 때문에 반응형임.
- 렌더링 중 계산된 변수도 반응형 → 무조건 의존성으로 넣어야 함.
- 예외: useState의 setState, useRef의 .current 등은 변하지 않는(안정된) 값이므로 제외 가능.
빈 의존성 배열이 의미하는 것
- []: 이 effect는 한 번만 실행됨 (마운트 시).
- 의존성이 없는 경우 -> 외부에서 불변의 값을 참조하고 있을 때만 사용.
- 반응형 값을 사용하고 있다면 절대 []로 두면 안 됨 -> 린터 에러 + 동기화 실패 가능.
React가 린터로 의존성이 올바른지 확인하는 방법
- 의존성 루프 등의 이유로 빠뜨리는 건 바람직하지 않으며, 반드시 코드 구조 자체를 개선해야 함.
린터에 동의하지 않을 때 해야 할 일
- // eslint-disable-next-line로 무시 X
- 해결법:
- 값을 컴포넌트 외부로 빼기 (정적이면 종속성 아님)
- effect를 쪼개서 독립적인 역할만 수행하게 만들기
- 이벤트 핸들러나 memoization(useCallback, useMemo)으로 구조 변경
정리
- 컴포넌트는 마운트, 업데이트, 언마운트할 수 있다.
- 각 effect는 주변 컴포너트와 다른 별도의 생명주기를 가진다.
- 각 effect는 시작 및 중지할 수 있는 별도의 동기화 프로세스를 지닌다.
- effect는 컴포넌트의 관점(마운트, 업데이트 또는 마운트 해제 방법)이 아닌 개별 effect의 관점(동기화 시작 및 중지 방법)에서 생각하자.
- 컴포넌트 본문 내부에 선언된 값은 “반응형”이다.
- 반응형 값은 시간이 지남에 따라 변경될 수 있으므로 effect를 다시 동기화해야 합니다
- 린터에 의해 플래그가 지정된 모든 오류는 합법적인 오류이다. 규칙을 위반하지 않도록 코드를 수정할 방법은 항상 있다.
6. Effect에서 이벤트 분리하기
이벤트 핸들러와 Effect 중에 선택하는 방법
사용자가 클릭 등 직접 상호작용한 경우 | 이벤트 핸들러 | 특정 시점에 명시적 실행되어야 함 |
컴포넌트가 렌더링된 결과에 따라 자동 동작해야 하는 경우 | Effect | 특정 상호작용 없이도 상태와 동기화 유지가 필요 |
이벤트 핸들러 | 비반응형 | 값이 변해도 재실행되지 않음. 사용자의 행위로만 실행 |
Effect | 반응형 | 의존성 값이 바뀌면 자동으로 재실행됨 |
useEffect(() => { connect(roomId) }, [roomId])처럼 작성하면, roomId가 바뀔 때 자동으로 동작
Effect 코드의 일부만 반응형이 아니길 원한다면 해야 할 것
- 문제: Effect내부에서 사용한 모든 반응형 값은 무조건 의존성 배열에 추가해야 함.
- 하지만 특정 값(ex: theme)은 동기화가 아니라 단순 참조만 원할 때도 있음.
- 해결책: 해당 로직을 useEffectEvent로 추출하면, 그 로직은 의존성 없이 최신 값만 사용함.
Effect 이벤트의 정의와 Effect에서 추출하는 방법
Effect 이벤트란?
- useEffectEvent(callback)으로 정의
- 항상 최신 props/state에 접근 가능
- Effect 내부에서만 호출 가능
- 이벤트 핸들러처럼 비반응형 동작으로 처리됨
const onConnected = useEffectEvent(() => {
showNotification('연결됨!', theme); // theme은 최신 값
});
useEffect(() => {
const conn = createConnection(serverUrl, roomId);
conn.on('connected', () => {
onConnected(); // Effect 이벤트 호출
});
conn.connect();
return () => conn.disconnect();
}, [roomId]); // theme을 의존성에서 제외해도 됨
|
Effect 이벤트를 사용해 Effect에서 최근의 props와 state를 읽는 방법
- 비동기 상황에서도 useEffectEvent를 통해 현재 최신값을 읽을 수 있음
- 예시: 장바구니 item 수(numberOfItems)는 변해도 방문 로그는 기록하고 싶지 않다면:
const onVisit = useEffectEvent((url) => {
logVisit(url, numberOfItems); // 항상 최신값 사용
});
useEffect(() => {
onVisit(currentUrl); // url만 반응
}, [currentUrl]);
|
=> numberOfItems는 의존성 배열에 넣을 필요 없음
정리
- 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행된다.
- Effect는 동기화가 필요할 때마다 실행된다.
- 이벤트 핸들러 내부 로직은 반응형이 아니다.
- Effect 내부 로직은 반응형이다.
- Effect의 비반응형 로직은 Effect 이벤트로 옮길 수 있다.
- Effect의 이벤트는 Effect 내부에서만 호출할 수 있다.
7. Effect 의존성 제거하기
Effect 의존성 무한 루프를 수정하는 방법
- Effect 안에서 사용한 모든 반응형 값(state, props 등) 은 의존성 배열에 포함해야 한다.
- 포함하지 않으면 React가 최신 값과 동기화하지 못해 버그 발생.
- 포함한 값이 자주 바뀌면 무한 루프 발생 가능성 있다.
- 단순히 배열에서 제거하면 안된다. 대신, 그 값이 반응형이 아님을 “증명”해야 한다.
- 예: props -> 컴포넌트 외부 상수로 옮기기
- 메시지 상태 -> setMessages(prev => [...prev, new])로 업데이트
Effect에 반응하지 않고 Effect에서 값을 읽는 방법
- useEffectEvent()를 사용해 Effect 이벤트로 로직 분리 (아직 개발중인 함수)
- 최신 상태값만 읽되, 의존성은 피할 수 있다.
객체와 함수 의존성을 피하는 방법과 이유
- JS에서 객체/함수는 참조가 달라지면 항상 새 값으로 간주된다.
- 불필요한 리렌더/재실행 발생
- 해결법:
-
컴포넌트 외부로 이동 값이 고정일 때 참조가 변하지 않으므로 안전 Effect 내부로 이동 반응형 값을 포함할 때 외부에서 새 객체 안 만들기 원시값 추출해서 의존성에 넣기 props로 객체를 받을 때 불필요한 재실행 방지
의존성 린터를 억제하는 것이 위험한 이유와 대신 할 수 있는 일
린터를 억제하기보다는
- 이벤트 핸들러로 로직 이동
- Effect 분리
- 비반응형 로직을 Effect 이벤트로 추출
정리
- Effect는 반응형 값을 기준으로 다시 실행됨 -> 의존성 배열과 코드가 항상 일치해야 한다.
- 의존성 배열이 “마음에 안 들면” -> 코드를 바꿔야 한다. 배열을 억지로 수정하지 말 것
- 목적별로 Effect를 분할하고, 비반응 로직은 이벤트로 분리하면 재실행을 제어할 수 있다.
8. 커스텀 Hook으로 로직 재사용하기
커스텀 Hook이란 무엇인지
- React의 Hook은 상태 관리나 사이드 이펙트를 함수형 컴포넌트에서 처리할 수 있게 해준다.
- 이 중 커스텀 Hook은 useState, useEffect 같은 기본 Hook을 조합하여 공통 로직을 재사용 가능한 함수로 분리한 것이다.
컴포넌트 간 로직 재사용하는 방법
- 여러 컴포넌트에서 동일한 기능(예: API 호출, 폼 상태 관리 등)을 사용할 때,
- 코드를 복붙하지 않고 공통된 비즈니스 로직을 하나의 함수로 분리하면 유지보수성과 가독성이 높아진다.
나만의 커스텀 Hook 이름 짓기와 구조 잡기
- 이름은 use로 시작해야 함 (React 규칙)
- 반드시 다른 Hook을 내부에서 호출해야 의미가 있다.
- 입력(props)과 출력(return)을 명확하게 설계
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('Mary');
const [lastName, setLastName] = useState('Poppins');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
<>
<label>
First name:
<input value={firstName} onChange={handleFirstNameChange} />
</label>
<label>
Last name:
<input value={lastName} onChange={handleLastNameChange} />
</label>
<p><b>Good morning, {firstName} {lastName}.</b></p>
</>
);
}
|
반복되는 로직은 아래와 같다.
- state 변화가 존재(firstname와 lastname)
- 변화를 다루는 함수 존재 (handleFirstNameChange와 handleLastNameChange)
- 해당 입력에 대한 value와 onChange 속성을 지정하는 JSX 존재
//useFormInput.js
import { useState } from 'react';
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}
//App.js
import { useFormInput } from './useFormInput.js';
export default function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
return (
<>
<label>
First name:
<input {...firstNameProps} />
</label>
<label>
Last name:
<input {...lastNameProps} />
</label>
<p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
</>
);
}
|
언제 커스텀 Hook을 추출할까 ?
- 여러 컴포넌트에서 동일한 로직이 반복될 때
- 사이드 이펙트 처리 로직이 복잡할 때
- 코드 분리가 필요할 때
'React' 카테고리의 다른 글
헤드리스 컴포넌트란? React에서 UI와 로직을 분리하는 방법 (0) | 2025.09.25 |
---|---|
React 학습하기 - State 관리하기 (3) | 2025.08.03 |
React 학습하기 - 상호작용 더하기 (3) | 2025.07.30 |
React 학습하기 - UI 표현하기 (2) | 2025.07.26 |
React 학습하기 - 빠르게 시작하기 (1) | 2025.07.21 |