대부분의 프론트엔드 코드는 비동기 작업을 하나의 형태로 취급합니다. Promise를 시작하고, resolve될 때까지 기다린 다음, 결과를 로컬 상태에 쏟아붓고 프레임워크가 diff를 조정(reconcile)하게 둡니다. 이 패턴은 REST 호출, 폼 제출, WebSocket 메시지 등 어디에서나 작동하기 때문에 매우 매혹적입니다. 이들은 모두 동일한 useEffect나 이벤트 핸들러에 도달하고, 모두 setState를 통해 파이프라인을 거치며, 모두 동일한 비동기 파이프라인처럼 보입니다. 하지만 이러한 획일성은 함정입니다. 실제 애플리케이션에서 모든 비동기 작업이 같지는 않습니다. 모든 작업이 같다고 가정하는 순간, 여러분의 UI 컴포넌트는 useEffect 훅과 요행에 기대어 짜깁기된, 의도치 않은 데이터 설계자로 전락하게 됩니다.
비동기 작업은 실제로 세 가지 다른 종류로 나뉩니다. 각 종류는 시간, 캐싱, 그리고 소유권과 서로 다른 관계를 맺고 있습니다. 이들을 구분하는 법을 배우는 것이 프론트엔드를 빠르고 정확하며 안정적으로 유지하는 비결입니다.
쿼리: 주소가 있는 사실
쿼리는 단순한 fetch가 아닙니다. 식별 가능한 사실에 대한 요청입니다. 여러분은 "어떤 사용자 데이터"가 아니라 /user/123을 요청하고 있는 것입니다. 이 차이는 중요합니다. 식별 가능성(identity)이야말로 캐싱을 가능하게 만드는 핵심이기 때문입니다. 같은 화면에 있는 두 컴포넌트가 동일한 사용자 레코드를 필요로 한다면, 하나의 답변을 공유해야 합니다. 각 컴포넌트가 useState에 자신만의 로컬 복사본을 유지하면, 데이터의 진실은 파편화됩니다. 헤더의 아바타와 사이드바의 이름이 서로 어긋나게 되는데, 이는 서로 다른 시간에 데이터를 가져왔거나, 하나는 성공하고 다른 하나는 실패했기 때문입니다.
쿼리를 액션이 아닌 리소스로 생각하십시오. 쿼리에는 캐시 키, 신선도 정책(freshness policy), 그리고 단일 컴포넌트보다 오래 지속되는 생명주기가 있습니다. 잘 설계된 쿼리 레이어는 /projects?page=2를 읽는 것이 /projects?page=3을 읽는 것과 다르다는 점을 이해합니다. 각 URL과 파라미터 세트는 하나의 주소를 형성하며, 해당 주소의 데이터는 오래되었거나(stale), 신선하거나(fresh), 혹은 없을 수 있습니다. UI가 이러한 장부 정리(bookkeeping)를 담당해서는 안 됩니다. UI는 데이터 레이어에 user:123을 요청하고 스냅샷을 받아야 합니다. 그 스냅샷이 2초 전에 서버에서 왔는지, 아니면 2밀리초 전에 캐시에서 왔는지는 컴포넌트가 상관할 바가 아닙니다.
그 실질적인 결과는 즉각적입니다. 모든 읽기 작업을 컴포넌트 내부의 명령형 fetch로 취급하면, 중복 제거(deduplication) 기능을 잃게 됩니다. 백그라운드 새로고침도 잃게 됩니다. 백그라운드에서 데이터를 검증하는 동안 캐시된 데이터를 즉시 보여주는 능력도 잃게 됩니다. 쿼리는 UI 트리 외부의 안식처를 가질 자격이 있습니다.
뮤테이션: 세상을 바꾸는 것
쿼리가 세상에 대해 묻는 것이라면, 뮤테이션은 세상을 바꿉니다. 실제 네트워크 요청인 POST, PUT, 또는 DELETE는 대개 쉬운 부분입니다. 진짜 어려운 부분은 서버가 "OK"라고 말한 이후에 일어나는 모든 일입니다.
사용자가 표시 이름을 업데이트한다고 가정해 봅시다. 뮤테이션 자체는 단일 요청입니다. 하지만 그 영향 범위(blast radius)는 도처에 퍼져 있습니다. 프로필 페이지에는 예전 이름이 남아 있습니다. 네비게이션 바에도 예전 이름이 표시됩니다. 댓글 기록이 이를 참조하고 있을 수도 있습니다. 만약 뮤테이션 코드가 로컬 isLoading 플래그를 토글하고 상태 한 조각을 업데이트하는 것 외에 아무것도 하지 않는다면, 여러분의 애플리케이션은 스스로에게 거짓말을 하고 있는 것입니다. UI의 일부는 변경 사항이 적용된 것처럼 행동하지만, 다른 일부는 변경 사항이 일어났다는 사실조차 모를 수 있습니다.
뮤테이션은 데이터 그래프에 미치는 영향을 반드시 선언해야 합니다. 어떤 쿼리가 이제 무효한지, 어떤 캐시 키를 다시 페치(refetch)해야 하는지, 그리고 어떤 관계가 변경되었는지를 시스템에 알려야 합니다. 이는 쿼리와 근본적으로 다릅니다. 쿼리는 읽기 전용이며 공유 가능합니다. 반면 뮤테이션은 쓰기 중심적이며 기존 캐시를 파괴합니다. 이 둘을 동일한 추상화로 단순화해 버리면, 개발자들은 결국 무작위 컴포넌트 내부에서 수동으로 refetch()를 호출하거나, 더 나아가 로컬 상태를 서버와 다시 동기화하기 위해 트리 곳곳에 useEffect 훅을 뿌려대게 됩니다.
소유권 모델 또한 다릅니다. 쿼리는 대개 캐시가 소유합니다. 뮤테이션은 이를 트리거한 사용자 액션이 소유합니다. 뮤테이션은 대기(pending) 상태, 에러 상태, 그리고 잠재적으로 롤백(roll)이 필요한 낙관적 값(optimistic value)을 가집니다.
