---
title: '리액트의 신규 훅, "use"'
tags:
  - react
  - javascript
published: true
date: 2023-06-13 23:51:18
description: '상황에 따라 이름이 변경되거나 사라질 수도 있습니다'
---

# Table of Contents

## 서론

리액트에는 새로운 기능을 제안할 수 있는 공식적인 창구인 https://github.com/reactjs/rfcs 저장소가 존재한다. 이 저장소는 리액트에 필요한 새로운 기능 내지는 변경을 원하는 내용들을 제안하여 리액트 코어 팀의 피드백을 받을 수 있는데, 이렇게 제안된 이슈 중에는 리액트 코어 팀이 직접 제안하여 리액트 커뮤니티 개발자들의 의견을 들어보는 이슈도 존재한다.

- 서버 컴포넌트: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md
- 서버 컴포넌트 모듈 컨벤션: https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md

이 중에 아직 머지되지는 않았지만 한가지 흥미로운 내용이 존재하는데, 바로 `use`라고 하는 새로운 훅이다. 이 훅은 이후에 설명하겠지만 이전의 훅과는 여러가지 차이점이 있는데, 그중에 하나는 조건부로 호출될 수 있다는 것이다. 이 훅도 예전부터 PR로 올라와 있어서 언제쯤 머지되는지 눈여겨 보고 있었는데 👀 도대체가 머지될 기미가 보이지 않아서 굉장히 의아한 차였다. 알고 보니 해당 proposal 을 만든 사람이 [meta에서 vercel로 이적하였고](https://github.com/reactjs/rfcs/pull/229#issuecomment-1427067863) (🤪) 이 과정에서 뭔가 이 작업이 붕뜬게 아닌가 하는 추측아닌 추측을 혼자 해봤다. 그러던 차에 리액트 카나리아 버전에서 `use`훅의 존재를 확인하게 되었다.

![react-use](./images/react-use.png)

https://www.npmjs.com/package/react/v/18.3.0-next-1308e49a6-20230330?activeTab=code

왠지 조만간 `use` 훅이 정식으로 등장할 날이 머지 않은 것 같아 이 참에 한번 다뤄보려고 한다. [react rfc에 있는 `First class support for promises and async/await`](https://github.com/reactjs/rfcs/pull/229) 을 읽어보고 `use`훅의 실체는 무엇인지 알아보자.

## 서버 컴포넌트의 등장

서버 컴포넌트의 등장으로 인해, 이제 다음과 같이 `async`한 컴포넌트를 만드는 것이 가능해졌다.

```javascript jsx
export async function Note({id, isEditing}) {
  const note = await db.posts.get(id)
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
      {isEditing ? <NoteEditor note={note} /> : null}
    </div>
  )
}
```

이와 같이 서버 자원에 직접 접근하여 서버의 데이터를 불러오는 서버 컴포넌트는 리액트 팀에서도 권장하는 방법이지만, 한가지 치명적인 사실은 대부분의 훅을 사용할 수 없다는 것이다. 물론, 서버 컴포넌트는 대부분 상태를 저장할 수 없는 기능에만 제한적으로 쓰이기 때문에 `useState`등은 필요하지 않을 것이며, `useId` 와 같은 훅은 서버에서도 여전히 사용 가능하다.

## 그렇다면 클라이언트 컴포넌트는?

이제 서버에서 쓰이는 함수형 컴포넌트가 `async`가 가능해진다... 라는 사실은 한가지 의문점을 갖게 한다. 그렇다면 클라이언트 컴포넌트가 비동기 함수가 되는 것은 불가능한 것인가? 지금까지 우리는 클라이언트 컴포넌트에서 비동기 처리를 하기 위해서는 `useEffect` 내에 비동기 함수를 선언하여 실행하는 것이 고작이었다. 그나마도, `useEffect`의 콜백 함수는 이러저러한 이유로 비동기가 되면 안되어 이상한 형태로(?) 만들어져서 사용되었다.

```javascript jsx
function ClientComponent() {
  useEffect(() => {
    async function doAsync() {
      await doSomething('...')
    }

    doAsync()
  }, [])

  return <>...</>
}
```

클라이언트 컴포넌트가 `async`하지 못한 것은 뒤이어 설명할 기술적 한계 때문이다. 그 대신, 리액트에서는 `use`라는 특별한 훅을 제공할 계획을 세운다.

## `use` 훅은 무엇인가?

`use` 훅의 정의에 대해서, rfc에서는 리액트에서만 사용되는 `await`이라고 비유했다. `await`이 `async`함수에서만 쓰일 수 있는 것 처럼, `use`는 리액트 컴포넌트와 훅 내부에서만 사용될 수 있다.

```javascript
import {use} from 'react'

function Component() {
  const data = use(promise)
}

function useHook() {
  const data = use(promise)
}
```

`use`훅은 정말 파격적이게도, 다른 훅이 할수 없는 일을 할 수 있다. 예를 들어 조건부 내에서 호출될 수도 있고, 블록 구문내에 존재할 수도 있으며, 심지어 루프 구문에서도 존재할 수 있다. 이는 `use`가 여타 다른 훅과는 다르게 관리되고 있음을 의미함과 동시에, 다른 훅과 마찬가지로 컴포넌트 내에서만 쓸 수 있다는 제한이 있다는 것을 의미한다.

```jsx
function Note({id, shouldIncludeAuthor}) {
  const note = use(fetchNote(id))

  let byline = null
  // 조건부로 호출하기
  if (shouldIncludeAuthor) {
    const author = use(fetchNoteAuthor(note.authorId))
    byline = <h2>{author.displayName}</h2>
  }

  return (
    <div>
      <h1>{note.title}</h1>
      {byline}
      <section>{note.body}</section>
    </div>
  )
}
```

그리고 이 `use`는 `promise`뿐만 아니라 `Context`와 같은 다른 데이터 타입도 지원할 예정이다.

그렇다면 이 `use`는 왜 만들어졌는지 좀더 자세히 살펴보자.

### `use`에 대한 의문

#### 왜 `async`가 아닐까?

많은 커뮤니티에서 요구헀던 것은, 서버 컴포넌트, 클라이언트 컴포넌트, share 컴포넌트에 관계 없이 비동기 컴포넌트 렌더링 시에 일관적인 방식을 제공하는 것이었다. 그러나 서버 컴포넌트와 다르게, 클라이언트의 경우 `async`를 사용하는데 있어 기술적인 제한사항이 존재했다.

이런 기술적 한계사항 외에도, 서버와 클라이언트에서 데이터에 접근하는 방식이 다르면 어떤 환경에서 작업하는지 조금 더 명확해진다는 장점이 있다. 물론 `use client`라는 지시자가 있지만, 이 지시자는 파일 맨위에 박혀있기 때문에 직관적으로 클라이언트 컴포넌트인지 알아채기 어렵다. 서버 컴포넌트는 클라이언트 컴포넌트와 비슷하지만, 한편으로는 너무 비슷하지 않았으면 한다고 언급했다. 각 환경에는 명확한 한계가 있으므로, 이를 빠르게 구별하면 개발자의 피로감을 줄이는데 많은 도움을 줄 수 있다. 즉, `async`로 선언되어 있는 컴포넌트는 서버 컴포넌트라는 명확한 신호를 줄 수 있다.

만약 미래에 클라이언트 컴포넌트에서도 `async`가 가능해지는 미래가 온다 하더라도 비동기 컴포넌트 (데이터를 가져오는 컴포넌트)와 상태를 가지고 있는 컴포넌트 (훅을 사용하는 컴포넌트)를 분리하여 리팩토링하도록 계속해서 권장할 예정이다. 리액트가 기대하는 것은, 데이터를 불러오는 컴포넌트와 상태를 가져오는 컴포넌트를 여러개로 분리하여 리팩토링하고, 필요하다면 서버로 작업을 옮기는 것이다.

#### `fetch`와 `read`의 불필요한 연결 방지

`await`과 `use`의 장점은 `promise`로 불러오는 비동기 데이터를 어떤식으로 불러오는지 전혀 관여하지 않는 다는 것이다. `await`과 `use`의 유일한 목적과 관심사는 데이터를 어떻게 가져오든지 간에, 단순히 비동기 데이터를 풀어서 가져오는 것 뿐이다.

원래 이전의 제안 내용은 `Suspense` 기반의 새로운 `fetching api`를 제공는 것이었는데 이렇게 되면 `fetch`와 `read`간에 강하게 연결되기 때문에, `fetch`와 렌더링이 불필요하게 연결된다는 문제가 존재했다.

그래서 리액트 팀은 현재 렌더링에 대해 영향을 미치지 않고, 데이터를 최적으로 가져올 수 있도록 단순히 `use`를 제공하는 방향으로 변경했다. `use`는 개발자가 직관적으로 사용할 수 있으며, 라이브러리와 상관없이 데이터를 가져오는 것이 훨씬더 자연스러워진다.

```jsx
function TooltipContainer({showTooltip}) {
  // 이 요청은 데이터를 블로킹하지 않는다.
  const promise = fetchInfo()

  if (!showTooltip) {
    // 여기로 올경우, `promise`가 끝나던 말던 상관없이 `null`을 반환한다.
    return null
  } else {
    // 여기로 오는 경우, `use`로 거친 `promise`가 끝날 때 까지 기다렸다가 렌더링이 시작된다.
    return <Tooltip content={use(promise)} />
  }
}
```

#### 리액트로의 유연한 전환

리액트 아키텍쳐가 많은 사랑을 받았던 이유중 하나는, 리액트 아키텍쳐는 단 하나만 존재하는 것이 아니며, 여러가지 서드파티 라이브러리와 프레임워크의 혁신과 혜택을 동시에 누릴 수 있다는 점이다. 만약 리액트가 여기에서 데이터를 불러오는 공식 api를 추가하게 되면, 많은 리액트 생태계에 혼란이 빚어질 것이다.

### 상세 설계

`use`는 `async/await`과 거의 동일한 프로그래밍 모델을 제공하도록 설계되어 있지만, `async/await`과 다르게 일반 함수형 컴포넌트나 훅에서도 여전히 작동한다. 자바스크립트 비동기 함수와 유사하게, 런타임은 일시 중단 및 재개를 위해서 내부 상태를 관리하겠지만, 컴포넌트 작성자의 관점에서 보면 순차적으로 실행되는 함수 처럼 보인다.

```jsx
function Note({id}) {
  // fetch 요청은 비동기이지만, 컴포넌트 작성자는 동기 동작처럼 작성할 수 있다.
  const note = use(fetchNote(id))
  return (
    <div>
      <h1>{note.title}</h1>
      <section>{note.body}</section>
    </div>
  )
}
```

자바스크립트 스펙에 따르면, `promise`의 `resolve` 값은 fulfill 또는 rejected 여부를 항상 비동기로만 확인할 수 있다. 데이터가 이미 로딩이 완료된 시점이라 할지라도, 그 값을 동기적으로 검사해서 확인할 방법이 없다. 이는 애매모호한 순서로 인한 데이터 경합을 피하기 위해, 자바스크립트 설계에서 의도적으로 마련한 장치다.

물론 이 설계의 동기 자체는 충분히 이해가 되지만, 리액트와 같이 `props`와 `state`를 기반으로 UI를 모델링하는 프레임워크에는 문제가 된다. 상황에 따라 리액트가 선택할 수 있는 방법은 다음과 같다.

- `promise`가 완료되기 전까지 잠시 일시정지 했다가 다시 컴포넌트를 렌더링하기: 만약 `use`로 넘겨받은 `promise`의 로딩이 끝나지 않았다면 예외를 던지고, 컴포넌트의 렌더링을 일시 중단한다. 그리고 `use`의 호출이 완료되면, 이 값을 반환한다. `async/await`과 다른 차이점은 일시정지된 함수 컴포넌트는 마지막 중단된 시점에서 다시 시작되는 것이 아니라는 사실이다. 즉, 런타임은 컴포넌트의 시작과 `use`로 인해 중단된 사이의 모든 코드를 다시실행 해야 한다. 이는 리액트 컴포넌트의 멱등성에 의존한다. 즉, 렌더링 중에 외부 부수 작용이 없으며, 주어진 state, props, context 등에 대해 동일한 결과를 반환한다. 성능 최적화를 위해 리액트는 일부 계산을 따로 메모이제이션 할수도 있다. 물론 이러한 방식은 `async/await` 대비 추가적인 오버헤드가 존재한다. 그러나 컴포넌트에 대한 데이터가 이미 확인된 경우 (데이터가 미리 로드 되었거나, 이와 관련없이 부모 컴포넌트 등으로 인해 리렌더링 되는 경우) `use`로 인한 마이크로 태스크 대기열을 기다리지 않고도 값을 가져올 수 있기 때문에 오버헤드가 적다.
- 이전 `promise` 결과 그대로 읽기: 만약 `props`나 `state`가 변경된 경우, `use`의 값이 이전과 같다고 보장할 수 없다. 이 경우 다른 전략을 취해야 한다. 가장 먼저해볼 수 있는 것은, 이전에 다른 `use` 또는 다른 렌더링 시도로 인해 해당`promise`를 읽어왔었는지 확인하는 것이다. 만약 한번이라도 읽은 적이 있다면, 굳이 일시 중단하지 않더라도 동기적으로 지난번 결과를 재사용할 수도 있다. 이를 위해 리액트는 `promise`객체에 몇가지 값을 더 추가했다.
  - `status` 필드에 `pending` `fulfilled` `rejected`
  - `promise`가 이행 (fulfilled) 되었다면, `value` 필드에 이행된 값을 채워둔다.
  - `promise`가 거절 (fulfilled) 되었다면,`reason` 필드에 거절된 이유, 에러 객체를 추가한다.

  한가지 명심해야 하는 것은, 모든 `promise`에 이 값을 추가하는 것은 아니라는 것이다. 단지 `use`를 사용하는 `promise`에 대해서만 이러한 값을 추가한다. 이 덕분에 `Promise.prototype`을 오염시키지 않아도 되며, 리액트가 아닌 코드에 영향을 미치지 않게 된다. 이는 물론 자바스크립트 표준은 아니지만, 리액트가 `promise`의 결과를 추
  적하는데 도움을 준다. 만약 미래에 `Promise.inpect`와 같이 동기적으로 `Promise`의 현재 상태를 알 수 있는 api가 제공된다면, 이를 사용할 의향도 있다.

- 관련 없는 업데이트 중에 `promise` 결과 읽기: `promise` 객체에서 결과를 추적하는 것은, `promise` 객체가 렌더링 중에 변경되지 않았을 때에만 유효한 전략이다. 만약 새로운 `promise`객체가 반환된다면, 이 전략은 통하지 않는다. 그러나 대부분의 경우에는, 새로운 `promise`객체라 할지라도, 이미 데이터를 가져온 경우가 많을 것이다. 아래 코드를 살펴보자.

  ```jsx
  async function fetchTodo(id) {
    const data = await fetchDataFromCache(`/api/todos/${id}`)
    return {contents: data.contents}
  }

  function Todo({id, isSelected}) {
    const todo = use(fetchTodo(id))
    return (
      <div className={isSelected ? 'selected-todo' : 'normal-todo'}>
        {todo.contents}
      </div>
    )
  }
  ```

  `id` 값이 변경되었다면, `fetchTodo`가 새로운 데이터를 반환하는 것이 맞다. 그러나 `isSelected`값만 변경된 경우는 어떤가? `use`에게 넘겨진 `promise`객체는 다르지만, 이미 과거에 불러온 데이터일 것이다. 만약 리액트가 이러한 경우를 제대로 처리하지 못한다면, 새로운 데이터를 요청한 적이 없음에도 불구하고 이로 인해 UI가 일시 중단될 수 있다. 따라서 이를 처리할 방법이 필요하다. 이 경우 리액트는 일시 중단 하는대신, 마이크로태스크 대기열이 완전히 빌 때 까지 기다린다. 그 동안 `promise`가 `resolved`되면, 리액트는 `Suspense` `fallback`을 트리거 하지 않고 즉시 컴포넌트 렌더링을 재가한다. 만약 그 기간 동안 `resolve`되지 않으면 새로운 데이터가 요청되었다고 가정하고 평소와 같이 일시 중지한다.

  > 이부분이 조금 어려울 수도 있어 부연 설명을 추가한다. `Promise`는 마이크로 태스크에서 해결된 다는점, 그리고 `Promise`는 이미 과거에 resolve된 적이 있는 데이터라면 (비록 동기적으로 상태를 알 수 없지만) 바로 값을 resolve 한다는 특성을 이용한 것이다.

  그러나 이것이 모든 문제의 해결책은 아니다. 이는 어디까지나 데이터 요청이 캐시된 경우에만 작동한다. 더 정확히 말하면, 새로운 입력이 없이 다시 리렌더링되는 비동기 함수는 반드시 마이크로태스크 시점 내에서만 해결되어야 한다는 제약 조건이 있다. 따라서 이 `use`는 `cache` API 와 함께 출시될 예정이다. `cache`가 없이 이 `use`가 출시될일은 거의 없다. 대략 `cache`는 아래와 같은 모습이 될 것이다.

  ```jsx
  // cache 함수로 래핑 되어 있다면, `input`이 동일하다면 이 함수는 항상 같은 결과를 반환한다.
  // cache는 아마도 `invalidate`하는 기능도 추가되어야 할 것이다.
  const fetchNote = cache(async (id) => {
    const response = await fetch(`/api/notes/${id}`)
    return await response.json()
  })

  function Note({id}) {
    // id가 변경되거나 캐시가 날아가지 않는한, 항상 같은 결과를 반환한다.
    const note = use(fetchNote(id))
    return (
      <div>
        <h1>{note.title}</h1>
        <section>{note.body}</section>
      </div>
    )
  }
  ```

  요즘 대부분의 fetch 라이브러리는 이미 이러한 이슈를 피하기 위한 캐싱 매커니즘을 구비하고 있으므로, 이 `cache`없이도 `use`를 사용할 수 있을 것이다. 다만 이러한 내용은 컴포넌트에서 비동기 함수를 직접 호출할 경우에 유용할 것이다.

#### 조건부 호출

다른 훅들과 다르게, `use` 훅은 앞서 소개한 훅들과 다르게 조건부로 호출할 수 있다. 이는 데이터를 별도 컴포넌트로 분리해서 추출하는 수고로움을 덜고, 조건부로 일시 중단 할 수 있도록 하기 위함이다.

이렇게 조건부로 `use`를 호출할 수 있는 이유는 대부분의 다른 훅과 달리 컴포넌트 업데이트에 따라서 상태를 추적할 필요가 없기 때문이다. `useState`와 같은 훅은 리액트 이전 상태와 연관지을 수 있도록 동일한 위치에서 조건부로 실행되는 일 없이 실행되어야 하지만, `use`는 컴포넌트를 일단 렌더링 한뒤에는 데이터를 저장할 필요가 없다. 저장하지 않는 대신, 데이터는 `promise`와 연관된다.

### 서버 컴포넌트에서 클라이언트 컴포넌트로 promise 넘겨주기

미래애 서버 컴포넌트에서 props 형태로 클라이언트 컴포넌트에 promise를 넘기는 기능을 추가하고자 한다. props로 넘겨주는 promise를 조건에 따라 호출하거나 제어할 수 있어 유용할 것이다.

#### `use`로 할 수 있는 다른 것들

다른 훅과는 다르게, `use`를 조건부로 호출할 수 있다는 사실이 개발자들에게 혼선을 빚을 수 있지만, 리액트 팀은 충분히 이 기능이 유용하기 때문에 이러한 혼란을 감수할 가치가 있다고 믿는 것 같다. 혼란을 줄이기 위해 리액트 팀은 이 훅이 유일하게 조건부 실행을 지원하는 훅이 될 것이라 약속했고, 개발자는 이 `use`의 특징 하나만 기억하면 될 것이다.

미래에 `use`는 `promise`외에도 다른 유형도 지원하게 될 것이다. 가장 먼저 `promise`외에 지원할 타입은 바로 `Context`다. 조건부로 호출할 수 있다는 점을 제외하면, 기존의 `useContext(Context)`와 동일하다.

## FAQ

### 왜 이름이 `use` 인가여? 좀더 구체적으로 해줄 순 없나요?

이유는 크게 두가지다.

- `use`는 `promise` 뿐만 아니라, `Context`, `store` `observable` 등 다양한 타입이 될 수 있기 때문이다.
- `use`는 조건부로 쓰일 수 있는 매우 특별한 훅이다. 위 종류에 따라 `usePromise` `useConditionalContext(?)` 등으로 도 할 수 있긴 하지만, 이 경우 조건부로 쓸 수 있는 훅을 외워야 하기 때문에 `use` 하나로 묶었다.

### 왜 컴포넌트에서만 호출 가능한가요?

`use`가 조건부로 호출은 되지만, 여전히 훅인 이유는 리액트가 렌더링 될 때만 동작할 수 있기 때문이다. 따라서 `use`는 컴포넌트 또는 훅일 수 밖에 없다. 이론적으로는 리액트 컴포넌트 내지는 훅에서만 호출되는 함수 내부에서 `use`를 사용하면 동작 자체는 하지만, 컴파일러에서 에러로 처리된다.

만약 일반 함수에서 사용할 수 있도록 허용된다면, 현재의 타입시스템으로는 이를 강제할 방법이 없기 때문에 이것이 올바른 문맥안에서 실행되고 있는지 추적하는 것은 온전히 개발자의 몫으로 남을 것이다. 이는 애초에 리액트 함수와 비 리액트 함수를 구별하기 위해 `use`라는 접두사를 만든 이유기도 하다. 즉, `use`라는 접두사를 강제 함으로써 개발자가 이러한 훅이 올바른 문맥에서 실행되는지 확인하는 수고로움을 더는 것이다.

### 왜 클라이언트 컴포넌트는 async가 안되나요

원래는 비동기 클라이언트 컴포넌트를 만드는 것 또한 고려했었다. 기술적으로 가능하긴 하지만, 여기에는 많은 함정과 주의사항이 뒤딸아오므로, 이패턴을 일반적인 권장사항으로 하기엔 무리가 있었다. 런타임에서는 이러한 비동기 클라이언트 컴포넌트를 지원하기는 할 것이지만, 개발중에는 경고를 기록할 것이다. 혼란을 방지하기 위해 문서에도 이러한 비동기 클라이언트 컴포넌트에 대한 내용도 기록하지 않을 것이다.

비동기 클라이언트 컴포넌트를 권장하지 않는 가장 큰 이유 중 하나는 `prop`이 컴포넌트를 메모이제이션을 무효화하여, 마이크로태스크 최적화가 꼬이기 너무 쉽기 때문이다.

그러나 비동기 클라이언트 컴포넌트가 유효한 시나리오가 있는데, 바로 네비게이션 중에서만 데이터를 업데이트 하는 경우다. 그러나 이러한 케이스를 보장하기 위해서는 라우터의 동작과 통합되어야 하는데, 이 경우 어느정도까지 문서화되어 관리되어야할지 불분명하다. 따라서 비동기 클라이언트 컴포넌트를 완전히 금지하지는 않았다. 이는 react-router 나 nextjs 등지에서 실험을 할 것으로 보인다.

## 요약

- 리액트의 렌더링을 일시 중지하고 재개할 수 있는 최적화가 추가되면서 클라이언트에서는 이를 어떻게 처리할지 많은 고민이 있었던 것으로 보인다.
- 클라이언트 컴포넌트와 서버 컴포넌트 간의 구별을 위해 (그리고 기술적인 이유, 개발자들의 편이성 등..) `await`과 `use`라는 또다른 구분점을 둔것으로 보인다. 얼핏 생각했을 때 이는 합리적인 선택으로 보인다.
- rfc에서도 언급했듯, `cache`가 등장하기 전까지 `use`가 등장하지는 않을 것으로 보인다. 그러나 `cache`는 아직까지 rfc에 모습조차 들어내지 않았기 때문에 당분간 모습을 보이긴 어려울 것으로 보인다.
- Promise 객체에 status를 추가하는 것은 조금,, 도발적으로 보이기도한다. 심지어 `Symbol`을 사용하는 것도 아니다. 이러한 작업으로 인해 향후에 표준과 얽히는 일이 없기만을 바랄 뿐이다.
- https://github.com/reactjs/rfcs/pull/229 에서 오가는 이야기를 보니 서버 컴포넌트와 마찬가지로 리액트 커뮤니티가 많은 혼란에 빠질 것 같다. 이번 18 버전의 많은 변화가 리액트에 있어 큰 변곡점이 될 지도 모른다. 더 좋은 웹을 만들기 위한 변화로 받아드릴지, 혹은 더 많은 리액트 반대 진영을 양산하는 결과를 만들어버릴지?
