
이걸 쓰기 위해 앞에 2개의 글을 작성했답니다..
그냥 깃허브 공개 코드였다면 속시원했을텐데..덕분에 다시 공부했어요~(p)

사용자가 직접 저장 버튼을 누르지 않아도, 데이터 변경이 감지되면 자동으로 서버에 저장되는 기능
대표적으로 Google Docs, Notion, Figma처럼 타이핑하는 순간 자동으로 저장되는 기능이 대표적이다.
Next.js 환경에서 그래픽 에디터 기능을 개발하면서 자동 저장의 필요성을 느꼈다.
에디터에서는 아래와 같이 작고 큰 다양한 변경들이 끊임 없이 발생했다.
이 과정에서 브라우저 종료, 새로고침, 네트워크 끊김 등 예상하지 못한 상황이 발생하면,
작업 내역이 모두 날아갈 수 있다.
그리고 실제로 기능 테스트를 진행하면서 브라우저 새로고침이 일어나면, 모든 데이터가 날라가는 상황에서 많이 짜증이 나기도 했다..ㅎㅎ
( 디자인과 출신으로서 정말 그냥 죽고 싶은 순간 1위 )

사용자가 매번 저장 버튼을 누르는 것도 불편하고, 깜빡하면 데이터 손실로 이어진다.
자동 저장은 이런 문제를 해결하기 위해 구현해보기로 했다.
자동 저장은 아래 두 가지 핵심 전략을 사용했다.
useMutation : 에디터 내부 작업물 데이터를 서버에 저장하는 요청 담당(▶ 이동하기)debounce : 불필요한 네트워크 요청을 줄이는 요청 제어 담당둘을 조합하면 아래와 같은 흐름을 만들 수 있다.
- 에디터 작업물 변경
debounce대기- 일정 시간 후
useMutation실행- 서버 저장
- 걱정 없이 행복한 에디터 작업 진행
연속된 이벤트 중 마지막 이벤트만 실행되도록 지연시키는 기법
만약 타이핑 중이라면, 타이핑을 멈추고(끝나고) n초 후에 실행한다고 이해하면 된다.
만약, debounce 없이 자동 저장을 구현한다면, 어떤 상황이 발생할 수 있을까?
debounce적용 전 - 총 5번의 API 요청이 일어난다.
1. "안" 입력 → API 요청
2. "녕" 입력 → API 요청
3. "하" 입력 → API 요청
4. "세" 입력 → API 요청
5. "요" 입력 → API 요청
debounce적용 후 - 적용 전과 다르게 1번의 API 요청만 발생한다.
1. "안녕하세요" 입력 → 1초 대기 → API 요청
정리하자면, debounce를 통해 자동 저장을 구현하면
불필요한 네트워크 요청을 최소화하여 서버 부하를 줄이고, 통신 비용을 낮춰 앱 성능을 높일 수 있다.
Next.js App Router에서 QueryClientProvider를 사용하려면 별도의 클라이언트 컴포넌트가 필요하다.
QueryClientProvider는 말그대로 Client Provider이기에 React Context 기반이기 때문이다.
즉, 서버 컴포넌트에서는 직접 사용할 수 없다.

'use client';를 사용하여 클라이언트 컴포넌트로 선언하여 Provider를 생성한다.
참고
useState로QueryClient를 생성하는 이유는 매 렌더링마다 새 인스턴스가 생성되는 것을 방지하기 위함이다.

서버 컴포넌트인 RootLayout에서 Context를 전달할 자식 요소(children)를 생성한 Provider로 감싼다.
이러면 이제 Next.js환경에서 Tanstak Query 세팅이 끝난다.
자동 저장 구현 전, API 요청 body와 Zustand store(CanvasState)를 동일한 JSON 구조로 설계했다.

구조를 동일하게 맞추면 별도 변환 없이 mutate(canvasState)으로 바로 넘길 수 있기때문이다.
백엔드와 응답 포맷을 사전에 조율한 덕분에 불필요한 작업 비용을 줄일 수 있었다.
에디터의 클라이언트 상태는 Zustand store로 관리한다.
CanvasState가 최상위 계층이며, 모든 에디터 데이터가 여기에 저장된다.

컴포넌트는 아래와 같이 store를 구독한다.

canvasState가 변경될 때마다 자동 저장이 트리거 된다.

useRef로 타이머를 관리하여 debounce를 구현했다.
useRef를 사용하는 이유는 타이머가 리렌더링 시에도 초기화되지 않고,
유지되어야 하기 때문이다.

참고
clearTimeout(timerRef.current)는
컴포넌트가 언마운트될 때 대기 중인 타이머를 취소한다.
호출하지 않으면 언마운트 후에도 뒤늦게 API 요청이 발생할 수 있다.
이번 프로젝트에서는 debounce를 3분으로 정하였다.
판단 기준은 지극히 개인적이긴하다. (개발자 실격 포인트)
1분으로 설정하면 너무 많은 통신 비용이 발생할 것 같고, 생각보다 1분동안 많은 작업양이 발생하지 않았다.
그렇지만, 5분동안은 생각보다 작업양이 많았다.
만약 자동저장이 안되어서 데이터를 날리더라도 타격이 없고, 통신 비용이 많지 않은 범위는 '3분'이 적당하다고 판단했다.
( just one 3 minutes..내 것이 되는 시간..순진한..작동에 기뻐 우는 유저들.. )
isPending, isError, isSuccess를 활용하여 저장 상태를 UI에 적용했다.
스타일링은 emotion/styled 기반으로 구현했다.


자동 저장 컴포넌트 코드는 아래와 같다.

스타일링은 emotion/styled의 css helper를 활용하여 구현했다.(▶ 이동하기)
이번 글을 정리하면 다음과 같다.
Next.js App Router에서 QueryClientProvider는 'use client' 컴포넌트로 분리하여 세팅한다.Zustand store(CanvasState)와 API 스키마를 동일한 JSON구조로 설계하면 별도 변환 없이 mutate(canvasState)로 바로 요청할 수 있다.useRef + setTimeout 조합으로 리렌더링 시에도 타이머를 유지하며 debounce를 구현한다.isPending / isError / isSuccess로 저장 상태를 UI에 표시 한다.이번 기회에 Tanstack Query의 공식 문서를 제대로 살펴보게 되었다.
그곳은 노다지다..끝나지 않은 배움의 축복이...눈물이 살짝 고인다(행복의 눈물..ㅎㅎ)