
React 19 공식 문서를 기반으로 진행한 몇 달 간의 스터디 내용을 시리즈로 정리하고자 한다.
이 시리즈는 hooks, 컴포넌트, API처럼 특정 기능 단위로 정리하기보다는,
React가 어떤 시점에 무엇을 실행하는지, 그리고 그 실행 시점과 동작 흐름 기준으로 내용을 묶는다.
공식 문서를 단순히 요약하는 것이 아니라,
각 개념을 읽으며 자연스럽게 생기는 “왜 이렇게 동작할까?”라는 질문을 출발로 React 전체를 이해하는 것을 목표로 한다.
따라서 공식 문서만으로 충분히 학습할 수 있는 기본적인 사용법은 다루지 않는다.
React에서는 지연 초기화(lazy initialization)를 제안한다.
useState(add())useState(add())는 렌더링마다 add()가 즉시 실행된다.
반환값은 첫 마운트 때만 초기 state로 사용되고 이후에는 무시되지만, 리렌더링 시에는 함수 실행 자체가 매번 발생한다.
add가 비싼 연산일 경우 불필요한 계산으로 성능 이슈가 생길 수 있다.
useState(add)useState(add)는 초기 state 계산 함수(initializer)로 취급되어 첫 마운트 시에만 add가 실행되고, 그 반환값이 초기 state로 사용된다. 리렌더링 시에는 함수 실행 자체가 발생하지 않는다.
이를 지연 초기화(lazy initialization)라 하며 성능 최적화에 유리하다.
useState(() => add())는 useState(add)와 같다.
add 함수의 반환값이 숫자일 경우 아래처럼 useState의 [상태값, 업데이트 함수] 타입이 같다.

부모 컴포넌트에서 전달하는 props의 상태가 변경되면 자식 컴포넌트는 리렌더링된다.
하지만 jsx컴포넌트가 아니라면?
function Parent() {
const [count, setCount] = React.useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
부모 상태 변경
</button>
{Child({ value: count })} // 함수를 실핼할 뿐
</div>
);
}
function Child({ value }) {
console.log('Child 렌더링됨'); // 실행은 되지만, React 컴포넌트로 관리되지 않음
return <div>값: {value}</div>;
}
React 기준에서 Child는 컴포넌트가 아니라 단순 함수 호출 결과이므로 재조정(reconciliation) 대상에서 제외된다
jsx문법으로 컴포넌트를 생성할 경우 React Fiber에 생성되어 관리된다. 그래서 일반 함수 실행시 등록되지 않은 컴포넌트는 리렌더링되지 않는다.
useState가 관리하고 있는 상태가 많고, 각 상태들의 관심사가 비슷하다면 useReducer를 사용하자.
예를들면, api로 받은 response data와 연관된 상태 값들...
const [data, setDate] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState({})
// api fetch 후 한 번에 변화되는 세 가지 상태값
// setData(data);
// setIsLoading(true);
// setError(error);
// 기타 등등...
만약 각 상태값들이 특정 연산이나 비즈니스 로직이 필요하다면 useCallback을 사용하여 최적화해야 할 수도 있다.
useReducer를 사용하면 다음의 이점이 생긴다.
같이 바뀌는 값들은 같이 바뀌게 한다.
// api fetch 후 한 번에 변화되는 세 가지 상태값
// isLoading, error는 내부에서 관리
const [state, dispatch] = useReducer(reducer, initialState);
const { data, isLoading, error } = state;
// api fetch 시작
dispatch({ type: 'FETCH_START' });
// api fetch 후 response를 받음
dispatch({ type: 'FETCH_SUCCESS', payload: data });
// error일 경우 error 전달
dispatch({ type: 'FETCH_ERROR', payload: err });

중첩된 자식 요소에 props drilling이 발생되었다면 context를 사용해보자.
내부 자식 요소에 필요한 값을 상속시켜 해결할 수 있다.
이는 CSS의 내부 요소들이 상위 요소에 선언된 값을 상속받아 사용하는 것과 동일하다.
공식 문서 - Using and providing context from the same component
Context : 특정 트리의 모든 컴포넌트가 공유하는 값을 정의할 수단
동적 스코프처럼 구현하여 내부의 모든 컴포넌트 내에서 동일하게 참조할 수 있도록 구현되어있다. 다이나믹하게 변경되는 값이지만 렌더 시점에서 동일하게 참조해야할 때, useContext와 함께 사용한다.
가장 가까이에 있는 <SomeContext>에서 선언된 값을 참조해서 내부 컴포넌트를 렌더한다.
useContext()가 값 읽음
useEffect는 비동기로 실행된다.
여기서 비동기는 흔히 promise, async와는 다른 미뤄두는 이벤트라고 생각하면 될 것 같다.
우리가 흔히 말하는 React에서의 '렌더링'은 UI 그리는 것(paint)이 아닌 컴포넌트 함수의 실행, VDOM 생성을 말한다.
만약 부모의 상태값이 리렌더링 된다면 내부 useEffect(passive) 순서는 어떻게 될까?
렌더링(VDOM 그리기) 시 부모 먼저 동작 후 자식이 동작하게 된다.
하지만 useEffect는 자식 useEffect작동 후 부모 useEffect가 작동된다. cleanup도 마찬가지.
이 순서가 보장되는 이유는 React Fiber commit 단계에서 passive effect를 DFS 후위 순회(post-order)로 실행하도록 설계했기 때문이다.
하지만 useEffect 내부에서 발생한 비동기 작업이나 DOM 이벤트의 실행 순서는 React가 보장하지 않는다.
effect 동작 순서
useLayoutEffect는 동기로 실행된다.
useLayoutEffect내부에서 state를 변경한다면 paint이전에 한 번 더 렌더링(VDOM 그리기)한다. 그래서 사용자는 변경점을 감지하지 못하고 변경되기 전 상태를 알 수 없으며, 최종 상태값으로 렌더링된다.
리렌더링시 effect 사이클
cleanup은 최초 마운트시에는 동작하지 않는다.
이후에 리렌더링시에는 이전 effect를 정리하고 새 effect를 실행하며, 언마운트되면 남아있는 마지막 effect을 정리한다.