03. Next.js에서 자동 저장 구현하기

flee | 플리·2026년 3월 22일

recode-TanstakQuery

목록 보기
3/3
post-thumbnail

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

0. Intro

0-1. 자동 저장이란?

사용자가 직접 저장 버튼을 누르지 않아도, 데이터 변경이 감지되면 자동으로 서버에 저장되는 기능

대표적으로 Google Docs, Notion, Figma처럼 타이핑하는 순간 자동으로 저장되는 기능이 대표적이다.

0-2. 왜 필요할까?

Next.js 환경에서 그래픽 에디터 기능을 개발하면서 자동 저장의 필요성을 느꼈다.

에디터에서는 아래와 같이 작고 큰 다양한 변경들이 끊임 없이 발생했다.

  • 작은 단위 변경 : 요소 1px 이동, 컬러 해시값 변경, 요소 내 컨텐츠 한 글자 변경 등
  • 큰 단위 변경 : 작업물의 한 페이지 삭제 및 추가, n개 이상의 요소의 그룹을 복사 등

이 과정에서 브라우저 종료, 새로고침, 네트워크 끊김 등 예상하지 못한 상황이 발생하면,
작업 내역이 모두 날아갈 수 있다.

그리고 실제로 기능 테스트를 진행하면서 브라우저 새로고침이 일어나면, 모든 데이터가 날라가는 상황에서 많이 짜증이 나기도 했다..ㅎㅎ
( 디자인과 출신으로서 정말 그냥 죽고 싶은 순간 1위 )

사용자가 매번 저장 버튼을 누르는 것도 불편하고, 깜빡하면 데이터 손실로 이어진다.

자동 저장은 이런 문제를 해결하기 위해 구현해보기로 했다.

0-3. 구현 전략 : debounce + useMutation 조합

자동 저장은 아래 두 가지 핵심 전략을 사용했다.

  • useMutation : 에디터 내부 작업물 데이터를 서버에 저장하는 요청 담당(▶ 이동하기)
  • debounce : 불필요한 네트워크 요청을 줄이는 요청 제어 담당

둘을 조합하면 아래와 같은 흐름을 만들 수 있다.

  1. 에디터 작업물 변경
  2. debounce 대기
  3. 일정 시간 후 useMutation 실행
  4. 서버 저장
  5. 걱정 없이 행복한 에디터 작업 진행

1. debounce란?

1-1. debounce 정의

연속된 이벤트 중 마지막 이벤트만 실행되도록 지연시키는 기법

만약 타이핑 중이라면, 타이핑을 멈추고(끝나고) n초 후에 실행한다고 이해하면 된다.

1-2. 자동 저장에서 debounce가 필요한 이유

만약, debounce 없이 자동 저장을 구현한다면, 어떤 상황이 발생할 수 있을까?

예시 상황 : input 창에 "안녕하세요" 타이핑

debounce 적용 전 - 총 5번의 API 요청이 일어난다.
1. "안" 입력 → API 요청
2. "녕" 입력 → API 요청
3. "하" 입력 → API 요청
4. "세" 입력 → API 요청
5. "요" 입력 → API 요청

debounce 적용 후 - 적용 전과 다르게 1번의 API 요청만 발생한다.
1. "안녕하세요" 입력 → 1초 대기 → API 요청

정리하자면, debounce를 통해 자동 저장을 구현하면
불필요한 네트워크 요청을 최소화하여 서버 부하를 줄이고, 통신 비용을 낮춰 앱 성능을 높일 수 있다.


2. Next.js App Router 환경 세팅

2-1. QueryClientProvider 세팅(App Router 기준)

Next.js App Router에서 QueryClientProvider를 사용하려면 별도의 클라이언트 컴포넌트가 필요하다.
QueryClientProvider는 말그대로 Client Provider이기에 React Context 기반이기 때문이다.
즉, 서버 컴포넌트에서는 직접 사용할 수 없다.

'use client';를 사용하여 클라이언트 컴포넌트로 선언하여 Provider를 생성한다.

참고
useStateQueryClient를 생성하는 이유는 매 렌더링마다 새 인스턴스가 생성되는 것을 방지하기 위함이다.

서버 컴포넌트인 RootLayout에서 Context를 전달할 자식 요소(children)를 생성한 Provider로 감싼다.
이러면 이제 Next.js환경에서 Tanstak Query 세팅이 끝난다.

참고
Tanstack Query 공식 문서

2-2. API 데이터 구조 설계

자동 저장 구현 전, API 요청 bodyZustand store(CanvasState)를 동일한 JSON 구조로 설계했다.

구조를 동일하게 맞추면 별도 변환 없이 mutate(canvasState)으로 바로 넘길 수 있기때문이다.
백엔드와 응답 포맷을 사전에 조율한 덕분에 불필요한 작업 비용을 줄일 수 있었다.


3. 자동 저장 구현

3-1. 에디터 client 상태 관리(Zustand)

에디터의 클라이언트 상태는 Zustand store로 관리한다.
CanvasState가 최상위 계층이며, 모든 에디터 데이터가 여기에 저장된다.

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

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

3-2. useMutation으로 저장 요청 연동

3-3. debounce 적용 - 불필요한 네트워크 요청 최소화

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

참고
clearTimeout(timerRef.current)
컴포넌트가 언마운트될 때 대기 중인 타이머를 취소한다.
호출하지 않으면 언마운트 후에도 뒤늦게 API 요청이 발생할 수 있다.

이번 프로젝트에서는 debounce3분으로 정하였다.

판단 기준은 지극히 개인적이긴하다. (개발자 실격 포인트)
1분으로 설정하면 너무 많은 통신 비용이 발생할 것 같고, 생각보다 1분동안 많은 작업양이 발생하지 않았다.
그렇지만, 5분동안은 생각보다 작업양이 많았다.
만약 자동저장이 안되어서 데이터를 날리더라도 타격이 없고, 통신 비용이 많지 않은 범위는 '3분'이 적당하다고 판단했다.

( just one 3 minutes..내 것이 되는 시간..순진한..작동에 기뻐 우는 유저들.. )

3-4. 저장 상태 UI 표시

isPending, isError, isSuccess를 활용하여 저장 상태를 UI에 적용했다.
스타일링은 emotion/styled 기반으로 구현했다.

4. 전체 코드

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

스타일링은 emotion/styledcss helper를 활용하여 구현했다.(▶ 이동하기)

5. 마무리..

이번 글을 정리하면 다음과 같다.

  • debounce로 연속된 상태 변경 이벤트를 제어하여 불필요한 네트워크 요청을 최소화한다.
  • Next.js App Router에서 QueryClientProvider'use client' 컴포넌트로 분리하여 세팅한다.
  • Zustand store(CanvasState)와 API 스키마를 동일한 JSON구조로 설계하면 별도 변환 없이 mutate(canvasState)로 바로 요청할 수 있다.
  • useRef + setTimeout 조합으로 리렌더링 시에도 타이머를 유지하며 debounce를 구현한다.
  • isPending / isError / isSuccess로 저장 상태를 UI에 표시 한다.

이번 기회에 Tanstack Query의 공식 문서를 제대로 살펴보게 되었다.
그곳은 노다지다..끝나지 않은 배움의 축복이...눈물이 살짝 고인다(행복의 눈물..ㅎㅎ)

profile
바라는 색이 있다면 눈이 멀도록 바라볼 것. 가능한 온몸으로 부서질 것.

0개의 댓글