첫 번째 난관, Concurrent Mode와의 씨름

13

발행일: 2025년 05월 12일

Zustand는 조금씩 사용자층을 넓혀가고 있었다. 간결함과 성능이라는 두 마리 토끼를 잡았다는 입소문이 퍼지면서, 개발자 커뮤니티의 변방에서 중심으로 조금씩 이동하는 중이었다. 다이시 카토는 밀려드는 질문에 답하고, 작은 버그들을 수정하며 보람찬 나날을 보내고 있었다.

하지만 평화는 오래가지 못했다. 오픈소스 생태계란 예측 불가능한 파도가 몰아치는 바다와 같은 곳. 잔잔한 수면 아래에는 언제나 새로운 도전이 도사리고 있었다.

그것은 GitHub 이슈 트래커에 등록된 하나의 보고서로부터 시작되었다.

[Bug] Zustand behaves unexpectedly in Concurrent Mode (Tearing issue?)

제목부터 심상치 않았다. ‘Concurrent Mode(동시성 모드)’. React 팀이 야심 차게 준비하고 있는 차세대 렌더링 전략. 마치 마법처럼 사용자 인터페이스의 반응성을 극대화하고, 무거운 작업을 백그라운드에서 처리하여 앱 전체가 멈추는 듯한 현상을 없애주는, React의 미래라 불리는 기술이었다.

“Concurrent Mode…?”

카토의 미간이 좁혀졌다. 그는 아직 정식 릴리즈되지 않은 실험적인 기능에 대해 깊이 파고들지는 않았었다. 하지만 언젠가는 마주해야 할 숙제라는 것은 알고 있었다.

이슈를 제기한 개발자는 상세한 재현 코드와 함께 문제를 설명했다. Concurrent Mode 환경에서, 특히 상태 변경이 잦고 렌더링이 중단되었다 재개되는 useTransition 같은 API와 함께 사용할 때, Zustand가 관리하는 상태가 일관성을 잃고 깨져 보이는 현상(Tearing)이 발생한다는 것이었다. 어떤 컴포넌트는 최신 상태를 보여주지만, 다른 컴포넌트는 이전 상태를 보여주는, 마치 화면이 찢어진 듯한 모습.

“말도 안 돼… 내 로직은 완벽했는데?”

카토는 처음에는 자신의 코드에 문제가 없다고 확신했다. 선택적 구독 메커니즘은 동기적인 환경에서는 완벽하게 작동했다. 하지만 Concurrent Mode는 달랐다. 렌더링이 더 이상 순차적이고 예측 가능한 과정이 아니었다. React는 필요에 따라 렌더링을 잠시 멈추거나, 우선순위를 조정하거나, 심지어는 중간 결과를 폐기하고 다시 시작할 수도 있었다.

마치 시간이 제멋대로 흘러가는 다른 차원에 들어선 기분이었다. 이런 환경에서는 기존의 동기적인 상태 구독 방식이 더 이상 안전하지 않았다. 렌더링 중간에 상태가 변경되거나, 서로 다른 시점의 상태를 참조하는 컴포넌트들이 뒤섞여 버릴 위험이 도사리고 있었다.

“이건… 보통 문제가 아니군.”

카토는 사태의 심각성을 깨달았다. 만약 Zustand가 React의 미래인 Concurrent Mode와 호환되지 않는다면, 그 생명력은 길지 않을 터였다. 아무리 지금 당장 편리하더라도, 미래가 없는 기술은 결국 도태될 운명이었다.

그날부터 카토는 다시 깊은 탐구의 세계로 빠져들었다. 그의 목표는 React의 내부 동작 원리, 그중에서도 Concurrent Mode의 스케줄링과 렌더링 메커니즘을 파헤치는 것이었다. 그는 React 팀의 공식 문서, 기술 블로그, 심지어는 소스 코드까지 뒤져가며 밤낮으로 연구에 몰두했다. useTransition, Suspense, 파이버 아키텍처(Fiber Architecture)… 생소하고 복잡한 개념들이 그의 머릿속을 휘저었다.

“상태 읽기의 일관성… 이게 핵심이군.”

며칠간의 씨름 끝에, 그는 문제의 본질에 도달했다. Concurrent Mode에서는 모든 컴포넌트가 렌더링 과정 동안 동일한 버전의 상태를 읽는 것이 무엇보다 중요했다. 외부 스토어를 사용하는 Zustand 같은 라이브러리는 React의 렌더링 주기와 외부 스토어의 상태 변경 시점 사이의 불일치를 해결해야만 했다.

해결책을 찾는 과정은 험난했다. 다양한 접근법을 시도했지만 번번이 벽에 부딪혔다. 구독 로직을 수정하고, 상태 업데이트 방식을 바꿔보았지만, Concurrent Mode의 예측 불가능한 동작은 계속해서 그의 발목을 잡았다. 사무실 책상 위에는 빈 커피 캔들이 쌓여갔고, 그의 눈은 점점 더 퀭해졌다.

‘포기할까… 아직 실험적인 기능인데…’

약한 마음이 고개를 들기도 했다. 하지만 그는 고개를 저었다. 여기서 물러설 수는 없었다. 이 난관을 극복해야만 Zustand는 진정으로 미래를 향해 나아갈 수 있었다.

그러던 어느 날 새벽, 여러 접근법을 조합하며 코드를 수정하던 중, 마침내 돌파구가 보였다. React가 외부 스토어와의 안전한 통합을 위해 제공하려는 새로운 훅(훗날 useSyncExternalStore로 발전하게 될 개념)의 초기 아이디어를 응용하는 방식이었다. 상태 변경 알림을 받는 방식과 컴포넌트가 상태를 읽는 시점을 React의 렌더링 주기에 맞춰 정교하게 동기화하는 것.

떨리는 손으로 코드를 수정하고, 다시 Concurrent Mode 환경에서 테스트를 실행했다. 이전에는 화면이 찢어지듯 불안정하게 흔들리던 애플리케이션이, 이제는 언제 그랬냐는 듯 부드럽고 일관되게 작동했다. 상태 값은 모든 컴포넌트에서 정확히 일치했다!

“됐다…! 드디어…!”

카토는 의자에 몸을 기댄 채 안도의 한숨을 내쉬었다. 온몸의 긴장이 풀리면서 극심한 피로감이 몰려왔지만, 마음만은 그 어느 때보다 충만했다. 첫 번째 큰 난관을 자신의 힘으로 돌파해낸 것이다.

이 씨름은 단순히 버그 하나를 잡은 것 이상의 의미를 가졌다. 이 과정을 통해 Zustand의 내부 구현은 훨씬 더 견고해졌다. 다양한 렌더링 환경에서도 안정적으로 동작할 수 있는 강인함을 갖추게 된 것이다. 마치 거센 폭풍우를 견뎌낸 나무가 더욱 깊고 튼튼하게 뿌리를 내리듯, Zustand는 이 시련을 통해 한 단계 더 성장했다.

카토는 해결된 이슈에 상세한 설명과 함께 수정된 코드를 커밋했다. 그리고 잠시 창밖을 바라보았다. 동이 트고 있었다. 새로운 아침이 밝아오고 있었지만, 그는 알고 있었다. 오픈소스 메인테이너의 길은 이제 막 시작되었을 뿐이라는 것을. 앞으로 또 어떤 예상치 못한 파도가 그를 기다리고 있을까? 그의 여정은 계속될 터였다.