비동기 Async 아톰의 세계

10

발행일: 2025년 05월 20일

의존성 그래프 관리, 메모리 누수 방지라는 두 개의 거대한 산맥을 넘어서자, 다이시 카토의 눈앞에는 또 다른 광활한 대지가 펼쳐졌다. 그것은 바로 ‘비동기(Asynchronous)’ 처리의 세계였다.

현대 웹 애플리케이션에서 상태는 더 이상 정적이지 않았다. 서버로부터 데이터를 가져오고(API 호출), 사용자의 입력에 따라 무언가를 저장하고, 복잡한 계산을 백그라운드에서 처리하는 등, 시간의 흐름 속에서 불확실하게 변하는 상태들이 넘쳐났다.

‘데이터 로딩 중…’, ‘오류 발생!’, ‘데이터 로딩 완료!’

이런 비동기 작업의 상태들을 어떻게 Jotai의 아토믹 모델에 자연스럽게 녹여낼 것인가? 이것은 단순한 기능 추가가 아니라, Jotai의 실용성을 결정짓는 매우 중요한 문제였다.

“지금까지의 아톰은… 너무 동기적이었어.”

카토는 기존 atom() 함수의 한계를 절감했다. 초기값을 받고, 동기적으로 값을 업데이트하는 구조. 여기에는 네트워크 지연이나 사용자 입력 대기 같은 ‘시간’의 개념이 들어설 자리가 없었다.

만약 API 호출 결과를 담는 원자를 만들고 싶다면?

// 기존 방식으로 비동기 데이터를 처리하려면...?
const userDataAtom = atom(null); // 초기값은 null
const isLoadingAtom = atom(false);
const errorAtom = atom(null);

function fetchUserData() {
  // 로딩 시작 상태 업데이트
  // API 호출
  // 성공 시 userDataAtom과 isLoadingAtom 업데이트
  // 실패 시 errorAtom과 isLoadingAtom 업데이트
  // ... 복잡하고 번거로운 로직 ...
}

이렇게 로딩 상태, 에러 상태, 실제 데이터 상태를 별도의 원자로 분리하고, 컴포넌트나 외부 로직에서 이들을 수동으로 관리해야 했다. 코드는 금세 지저분해지고, 상태 간의 일관성을 유지하기도 어려웠다. 마치 여러 개의 시계를 동시에 맞춰야 하는 것처럼 번거롭고 오류 발생 가능성이 높았다.

“이건 아니야… 더 우아한 방법이 필요해.”

그는 다시 깊은 고민에 빠졌다. 어떻게 하면 비동기 작업 자체를 하나의 ‘원자’처럼 다룰 수 있을까? 원자 하나가 비동기 작업의 전 과정을 내포하고, 그 상태(로딩, 성공, 실패)를 스스로 관리하며, 필요한 컴포넌트에게 적절한 시점에 값을 전달하는 방식.

그의 머릿속에서 새로운 종류의 아톰 함수가 구체화되기 시작했다. 이름하여 atomWithAsync (지금은 atom과 lodable을 결합해 사용한다).

이 함수는 비동기 작업을 수행하는 함수(예: async () => await fetch('/api/user'))를 인자로 받는다. 그리고 이 함수가 반환하는 것은 단순한 데이터 값이 아니라, 비동기 작업의 현재 상태를 나타내는 특수한 객체 혹은 값이어야 했다.

// atomWithAsync 개념 스케치
const userDataAtom = atomWithAsync(async (get) => {
  // 필요하다면 get 함수로 다른 아톰 값에 접근하여 API 호출에 활용 가능
  const userId = get(userIdAtom);
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error('Failed to fetch user data');
  }
  return response.json();
});

// 컴포넌트에서는 이렇게 사용 (Suspense와 함께라면 더욱!)
function UserProfile() {
  const userData = useAtomValue(userDataAtom); // 비동기 아톰 값 읽기

  // Jotai가 내부적으로 로딩 상태를 처리하고,
  // React Suspense와 연동하여 로딩 UI를 보여줄 수 있다면?
  return (
    <div>
      <h1>{userData.name}</h1>
      <p>{userData.email}</p>
    </div>
  );
}

// Suspense로 감싸기
<Suspense fallback={<div>Loading user...</div>}>
  <UserProfile />
</Suspense>;

“그래… Suspense!”

카토는 React의 Suspense 기능과의 연동 가능성에 주목했다. Suspense는 데이터 로딩과 같은 비동기 작업을 컴포넌트 렌더링 과정에 통합하여, 선언적으로 로딩 상태 UI(fallback)를 표시할 수 있게 해주는 강력한 기능이었다.

만약 atomWithAsync가 내부적으로 비동기 작업의 상태를 추적하고, 데이터가 아직 준비되지 않았을 때 Suspense에게 ‘아직 기다려!’라는 신호(Promise를 throw하는 방식 등)를 보낼 수 있다면? 개발자는 더 이상 isLoading, error 같은 상태를 직접 관리할 필요가 없어진다! 그저 비동기 아톰을 useAtomValue로 읽기만 하면, 나머지는 Jotai와 React가 알아서 처리해주는 마법 같은 경험을 제공할 수 있을 터였다.

“데이터 로딩 상태 관리가… 이렇게 쉬워질 수 있다고?”

그는 흥분을 감추지 못했다. 이것은 단순히 코드를 줄이는 것을 넘어, 비동기 UI 개발의 패러다임 자체를 바꿀 수 있는 잠재력을 가지고 있었다. 보일러플레이트는 사라지고, 개발자는 오직 비즈니스 로직과 최종 결과 UI에만 집중할 수 있게 된다.

물론 구현은 또 다른 도전이었다. Promise의 상태를 추적하고, Suspense와의 호환성을 맞추고, 에러 처리와 캐싱 전략까지 고려해야 했다. 그는 다시 한번 복잡한 비동기 세계와 React 내부 메커니즘을 탐험하기 시작했다. 시행착오를 거듭하며 atomWithAsync 헬퍼 함수의 프로토타입을 만들어 나갔다.

비동기 아톰. 이것은 Jotai가 단순한 상태 ‘저장소’를 넘어, 복잡한 데이터 흐름까지 우아하게 관리할 수 있는 ‘플랫폼’으로 진화하는 중요한 전환점이었다. 동기적인 세계를 넘어 비동기적인 현실까지 품을 수 있게 된 것이다.

이제 Jotai는 기본적인 원자 정의, 파생 상태 계산, 의존성 추적, 메모리 관리, 그리고 비동기 처리까지, 상태 관리의 핵심적인 영역들을 아우르는 모습을 갖춰가고 있었다. 남은 것은 이 모든 조각들을 하나의 완성된 그림으로 꿰맞추고, 세상의 엄격한 검증을 받을 준비를 하는 것이었다. 카토는 다가올 더 큰 도전을 예감하며, 다시 한번 심호흡을 했다.