use
는 Promise나 context와 같은 데이터를 참조하는 React 훅이다.
다른 React Hook과 달리 use
는 if와 같은 조건문과 반복문 내부에서 호출할 수 있다.
💡
use
Hook은 현재 React Canary 및 실험적 채널에서만 사용할 수 있다.
- React 19 버전에서 정식 출시될 예정이다.
Promise와 함께 호출될 때 use
Hook은 Suspense, ErrorBoundary와 통합하여 사용할 수 있다.
use
에 전달된 Promise가 pending되는 동안 use
를 호출하는 컴포넌트는 suspend가 된다.
이 때 컴포넌트가 Suspense로 감싸져 있다면 Fallback이 렌더링되고, Promise가 resolve된다면 Suspense 내부 컴포넌트가 렌더링된다.
만약 reject된다면 가장 가까운 ErrorBoundary의 Fallback이 렌더링된다.
use
Hook은 컴포넌트나 Hook 내부에서 호출되어야 한다.
서버 컴포넌트에서 데이터를 fetch할 때는 use
보다 async/await
을 사용한다.
async/await
은 await
이 호출된 시점부터 렌더링을 시작하는 반면, use
는 데이터가 리졸브된 후에 컴포넌트를 리렌더링한다.클라이언트 컴포넌트에서 Promise를 생성하는 것보다 서버 컴포넌트에서 Promise를 생성하여 전달하는 것이 좋다.
use
는 try-catch
블록에서 호출할 수 없다. 대신 Error Boundary
로 래핑하거나 Promise의 catch 메서드
를 사용하여 대체 값을 제공해야 한다.
use
는 렌더링 중에 생성된 Promise를 지원하지 않는다.
export type Usable<T> = Thenable<T> | ReactContext<T>;
function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
if (typeof usable.then === 'function') {
// thenable 객체인 경우
const thenable: Thenable<T> = (usable: any);
return useThenable(thenable);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
// Context 객체인 경우
const context: ReactContext<T> = (usable: any);
return readContext(context);
}
}
// Promise, Context가 아닌 경우
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}
인자로 들어온 usable 객체가 thenable 객체인 경우 useThenable
함수를 호출하고, context 객체인 경우에는 readContext
함수를 호출하는 것을 볼 수 있다.
let thenableIndexCounter: number = 0;
let thenableState: ThenableState | null = null;
export function createThenableState(): ThenableState { // Thenable 상태 초기화
return [];
}
function useThenable<T>(thenable: Thenable<T>): T {
// 각 Promise 객체에 고유한 인덱스를 할당하여 구분
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
// Thenable 상태가 없다면 초기화(단순히 배열을 생성하는 것이다)
thenableState = createThenableState();
}
const result = trackUsedThenable(thenableState, thenable, index);
const workInProgressFiber = currentlyRenderingFiber; // 현재 렌더링 중인 Fiber
const nextWorkInProgressHook =
workInProgressHook === null
? workInProgressFiber.memoizedState
: workInProgressHook.next; // 다음 작업 중인 Hook을 식별
if (nextWorkInProgressHook !== null) {
} else {
// 훅의 디스패처 설정
const currentFiber = workInProgressFiber.alternate;
// 컴포넌트가 마운트, 업데이트 되는지에 따라 적절한 디스패처 선택
ReactSharedInternals.H =
currentFiber === null || currentFiber.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
return result; // Promise에서 해결된 값이거나 아직 해결되지 않은 값일 수 있다.
}
trackUsedThenable
함수를 호출한다.💡 thenableIndexCounter와 thenableState는 각 렌더링마다 초기화된다.
컴포넌트가 렌더링할 때 실행되는renderWithHooks
함수 끝에finishRenderingHooks
함수를 매번 호출하여 상태를 초기화시킨다.export function renderWithHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any { finishRenderingHooks(current, workInProgress, Component); return children; } // 상태 초기화 function finishRenderingHooks<Props, SecondArg>( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, ): void { thenableIndexCounter = 0; thenableState = null; }
function getThenablesFromState(state: ThenableState): Array<Thenable<any>> {
// dev mode code..
const prodState = (state: any);
return prodState;
}
function noop(): void {} // 빈 함수(핸들러 추가용, 메모리 누수 방지)
export function trackUsedThenable<T>(
thenableState: ThenableState,
thenable: Thenable<T>,
index: number
): T {
// 인덱스에 해당하는 thenable를 추적한다.
const trackedThenables = getThenablesFromState(thenableState);
const previous = trackedThenables[index];
if (previous === undefined) {
// 새로운 thenable이라면 추적한다.
trackedThenables.push(thenable);
} else {
if (previous !== thenable) {
// 기존과 다르면 이전 thenable를 유지한다.
// noop 함수는 빈 함수로, thenable에 핸들러를 추가하여 메모리 누수를 방지한다.
thenable.then(noop, noop);
thenable = previous;
}
}
switch (thenable.status) {
case "fulfilled": {
// 완료된 상태라면 그 값을 반환한다.
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case "rejected": {
// 거부된 상태라면 오류를 검사한 후 던진다.
const rejectedError = thenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
default: {
// 대기 상태 등등...
if (typeof thenable.status === "string") {
// 상태가 문자열이라면 noop 핸들러를 추가한다.
thenable.then(noop, noop);
} else {
const root = getWorkInProgressRoot(); // 트리 가져오기
if (root !== null && root.shellSuspendCounter > 100) {
// suspense 관련 작업의 수가 100을 초과한 경우 에러 처리
throw new Error(
"async/await is not yet supported in Client Components, only " +
"Server Components. This error is often caused by accidentally " +
"adding `'use client'` to a module that was originally written " +
"for the server."
);
}
// 상태를 pending으로 설정하고 성공과 실패에 대한 핸들러 추가
const pendingThenable: PendingThenable<T> = (thenable: any);
pendingThenable.status = "pending";
pendingThenable.then(
(fulfilledValue) => {
if (thenable.status === "pending") {
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = fulfilledValue;
}
},
(error: mixed) => {
if (thenable.status === "pending") {
const rejectedThenable: RejectedThenable<T> = (thenable: any);
rejectedThenable.status = "rejected";
rejectedThenable.reason = error;
}
}
);
}
switch ((thenable: Thenable<T>).status) {
case "fulfilled": {
// 성공한 경우 값 반환
const fulfilledThenable: FulfilledThenable<T> = (thenable: any);
return fulfilledThenable.value;
}
case "rejected": {
// 실패한 경우 오류 처리 및 throw
const rejectedThenable: RejectedThenable<T> = (thenable: any);
const rejectedError = rejectedThenable.reason;
checkIfUseWrappedInAsyncCatch(rejectedError);
throw rejectedError;
}
}
// Suspense 예외를 throw
suspendedThenable = thenable;
throw SuspenseException;
}
}
}
해당 thenable이 추적 배열에 존재하면 이전 thenable로 대체하고 존재하지 않다면 추적 배열에 추가한다.
thenable 상태가 fulfilled
라면 값을 반환하고, rejected
라면 에러를 던진다.
pending
상태라면 빈 핸들러를 추가하여 메모리 누수를 방지한다.
위 조건에 부합하지 않는 경우(커스텀 thenable 객체)라면 현재 트리의 루트를 가져와 shellSuspendCounter
(suspense 관련 작업의 수)를 체크하고 100을 초과한다면 에러를 던진다.
커스텀 thenable에는 상태가 없기 때문에 상태를 추가하고 성공 or 실패했을 때 동작할 핸들러(상태 변경, 값 반환 등)를 추가한다.
상태가 fulfilled
라면 값을 반환하고, rejected
라면 에러를 던진다.
위 조건에 부합하지 않는 경우(pending 상태) Suspense 상태로 간주하여 suspendedThenable
에 thenable을 저장하고 SuspenseException를 throw한다.
💡
noop
함수를 핸들러에 추가하는 이유
Promise에.then()
or.catch()
핸들러가 없으면, 그 Promise는 가비지 컬렉션이 지연될 수 있어 메모리 누수의 원인이 될 수 있다.
noop
이라는 빈 함수를 핸들러에 추가함으로써 Promise가 해결되거나 거부될 때 처리된 것으로 간주하고 필요 없어졌을 때 가비지 컬렉션의 대상으로 만들 수 있다.
이 함수의 동작을 크게 5가지로 나눌 수 있다.
새로운 thenable이라면 추적 배열에 추가하고, 아닌 경우 이전 thenable로 현재 thenable을 대체한다.
thenable 객체 상태에 따라 값을 반환하거나 에러를 던지거나 빈 핸들러를 추가한다.
현재 작업중인 루트 트리를 가져와 shellSuspendCounter
체크해서 100을 초과하면 에러를 던진다.
커스텀 thenable 객체에 상태를 추가하고 이 후 동작에 대한 핸들러(상태 변경, 값 반환 등)를 추가하고 상태를 확인하여 값을 반환하거나 에러를 던진다.
아직 thenable이 해결되지 않은 경우 Suspense 상태로 간주하여 suspendedThenable
에 thenable을 저장하고 SuspenseException를 throw한다.
처음에 Promise 객체를 다른 말로 thenable 객체라고 부르는 줄 알았다..
Promise 객체
then
, catch
, finally
등의 표준 메서드를 가진다.pending
, fulfilled
, rejected
중 하나의 상태를 가진다.thenable 객체
then
메서드를 가진 객체를 의미한다.thenable은 Promise의 개념은 일반화한 것으로 Promise보다 더 넓은 범위의 객체를 포함할 수 있다.(React의 내부 구현에서 thenable을 사용하는 것은 더 유연한 비동기 처리를 위함이다)
➔ Promise 객체가 thenable한 객체이지만, thenable 객체가 무조건 Promise 객체인 것은 아니다.
export function readContext<T>(context: ReactContext<T>): T {
return readContextForConsumer(currentlyRenderingFiber, context);
}
let lastContextDependency: ContextDependency<mixed> | null = null;
function readContextForConsumer<T>(
consumer: Fiber | null, // context를 사용하는 컴포넌트(Fiber)
context: ReactContext<T> // context 객체
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2; // 렌더러에 맞는 값 설정
if (lastFullyObservedContext === context) {
} else {
// 새로운 context 의존성 객체 생성
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
if (lastContextDependency === null) {
// 첫 번째 context 의존성인 경우
if (consumer === null) {
// 잘못된 위치에서 Context를 읽으려 할 때 에러 발생
throw new Error(
"Context can only be read while React is rendering. " +
"In classes, you can read it in the render method or getDerivedStateFromProps. " +
"In function components, you can read it directly in the function body, but not " +
"inside Hooks like useReducer() or useMemo()."
);
}
lastContextDependency = contextItem;
consumer.dependencies = {
// consumer의 dependencies 설정
lanes: NoLanes,
firstContext: contextItem,
};
} else {
// 이미 의존성이 있는 경우 새 의존성을 체인의 끝에 추가
lastContextDependency = lastContextDependency.next = contextItem;
}
}
return value; // context 값 반환
}
readContext
함수만으로는 React Context의 내용을 이해하기 어려울 것 같은데 이전에 따로 React Context의 내부 동작 원리에 대해 정리했던 게 있어서 이걸 참고해도 좋을 것 같다.
간단하게 설명하면 Context Provider를 만나면 리액트 내부적으로 스택 자료구조에 value를 푸쉬하고 Provider 범위를 벗어나면 pop하여 이전 상태로 돌아간다.
컴포넌트 내부에서 readContext
함수가 호출되면 해당 Context 객체의 value 값을 가져온다.
use
Hook은 thenable 객체와 Context 객체를 처리할 수 있다.
thenable 객체
Context 객체
React에서 제공하는 대부분의 기본 Hook들은 Fiber 노드 내에 연결 리스트(Linked List)로 구성되어 각 노드는 하나의 Hook에 해당하며, Hook의 상태와 관련 정보를 포함한다.
useState로 예를 들면 초기 마운트 단계에서 mountWorkInProgressHook
를 호출하여 새로운 Hook 노드를 생성하고 연결 리스트에 추가한다. 이 Hook 노드에 초기 상태 값을 저장한다.
업데이트 단계에서 updateWorkInProgressHook
를 호출하여 기존 Hook 노드를 찾아 업데이트한다. 이 과정에서 이전 상태 값을 읽고 새로운 상태 값을 설정한다.
이처럼 React는 호출된 순서대로 연결 리스트에 저장하고 관리한다. 매 렌더링마다 같은 순서로 Hook이 호출되어야 React가 올바른 Hook 노드를 찾아 데이터를 관리할 수 있기 때문에 useState와 같은 React Hook들은 호출 순서가 중요한 것이다.
하지만 useThanble
은 데이터가 Promise 자체에 붙어있고, readContext
도 데이터는 Context Provider의 가장 가까운 Fiber 노드에서 가져온다.
그렇기 때문에 use
Hook은 useThanble
과 readContext
를 호출하기 때문에 조건문이나 반복문에서 호출될 수 있는 것이다.
🤷♂️ 그러면
readContext
를 호출하는useContext
도 조건문이나 반복문에서 사용할 수 있지 않을까?
➔useContext
도 조건문이나 반복문 내부에서 사용해도 동작하는데 문제가 없지만 Lint Rule에서 경고를 표시한다.
use 훅이 바꿀 리액트 비동기 처리의 미래 맛보기
리액트의 신규 훅, "use"
use - React 공식 문서
React 19 RC - React 공식 문서
RFC: First class support for promises and async/await
text/0000-first-class-support-for-promises.md
How does use() work internally in React?