0352

FE

[SvelteKit] Svelte + Firestore로 구현한 ‘자동 저장’ 기능 구축기

| “저장 버튼을 누르기 전에 브라우저가 닫혔습니다.”

안녕하세요 팡일입니다!

혹시 이런 경험 있으신가요? 상상만 해도 아찔한 상황입니다. 몇 시간 동안 공들여 작성한 글이 예기치 못한 브라우저 오류나 탭 닫기 한 번에 사라진다면, 누구라도 깊은 허탈감을 느낄 수밖에 없습니다.

그리고 이 경험은 결국 이렇게 이어집니다.

“이 서비스는 내 시간을 존중하지 않는구나.”

사용자 경험에서 이런 감정은 치명적입니다. 그래서 저는 이번 프로젝트에서 자동 저장(auto save) 초안(Draft) 기능을 통해 사용자의 시간을 지켜주는 기능을 구현했습니다. 사용자가 글을 작성하는 동안 조용히 뒷단에서 저장해주고, 다시 돌아왔을 때 “이어서 작성하시겠어요?”라고 묻는 친절한 경험을 제공하는 것이 목표였습니다.

이 글에서는 Svelte와 Firestore를 활용해 안정적이고, 빠르고, 유저 친화적인 자동 저장 기능을 실제로 어떻게 구현했는지 그 과정을 공유하겠습니다.

| 목표 정의: 좋은 Draft 시스템이 갖춰야 할 조건

자동 저장은 단순히 ‘저장 한번 해주는 기능’이 아닙니다. 사용자가 진짜로 믿고 쓸 수 있는 기능이어야 했습니다. 그래서 다음과 같은 목표를 세웠습니다.

  1. 자동 저장: 사용자가 타이핑을 멈추면 일정 시간 뒤 자동으로 임시 저장한다.

  2. 불러오기: 페이지에 다시 방문했을 때 이전 초안을 감지하고 불러올지 선택권을 제공한다.

  3. 정리(Cleanup): 사용자가 최종 제출하면 초안은 자동으로 삭제하여 깔끔한 상태를 유지한다.

  4. 심리적 안정감: 자동 저장이 있더라도 ‘임시 저장’ 버튼을 제공하여 사용자가 스스로 저장할 수 있는 심리적 여유를 제공한다.

| 구현 과정

1) 1단계: 초안을 저장할 공간 — Firestore의 drafts 컬렉션

자동 저장 기능의 핵심은 “신뢰성 있는 저장 공간”입니다. 저는 Firestore에 drafts라는 전용 컬렉션을 만들고 문서 ID를 userId로 지정했습니다.

code
/drafts/{userId}

이 구조 덕분에 특정 사용자의 초안을 불러오거나 지우는 것이 매우 간단해졌습니다.

서비스 레이어에는 다음 세 가지 기능을 구현했습니다.

JavaScript
// 초안 저장 또는 덮어쓰기
async function saveDraft(userId, draftData) { ... }

// 초안 불러오기
async function getDraft(userId) { ... }

// 초안 삭제
async function deleteDraft(userId) { ... }

2) 2단계: 자동 저장의 두뇌 — debounce

문제는 여기서 시작됩니다. onInput마다 저장 함수를 호출해 버리면, 몇 초 만에 Firestore에 수백 번의 요청이 발생합니다. 이는 비용·성능·안정성 측면에서 모두 최악입니다.

그래서 도입한 기술이 바로 debounce입니다.

“연속되는 이벤트 중 마지막 이벤트가 끝난 후 일정 시간이 지나면 단 한 번만 실행한다.”

자동 저장에 완벽히 어울리는 메커니즘입니다.

JavaScript
function debounce(func, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func(...args), delay);
  };
}

이제 writeStore의 내용이 변경될 때마다 debounce된 저장 함수를 호출합니다.

HTML
<script>
  const debouncedSave = debounce(async () => {
    await saveDraft(userId, $writeStore);
    // "자동 저장되었습니다." 같은 토스트를 띄우면 UX가 더 좋아집니다.
  }, 3000);

  writeStore.subscribe(debouncedSave);
</script>

결과적으로 사용자가 아무리 빠르게 타이핑해도 딱 ‘타이핑을 멈춘 시점 + 3초 뒤’에만 저장이 실행됩니다.

3) 3단계: UX를 완성하는 기능 — 불러오기 & 삭제

(1) 불러오기 (Restore)

: 사용자가 다시 글쓰기 페이지에 진입했을 때 자동 저장된 글이 있다면, 반드시 이를 알려야 합니다.

code
onMount(async () => {
  const draft = await getDraft(userId);

  if (draft) {
    if (confirm("작성 중이던 글이 있습니다. 이어서 작성하시겠습니까?")) {
      hydrateWriteStore(draft);
    }
  }
});

이 한 줄의 confirm 모달이 사용자의 시간을 지켜줍니다.

(2) 삭제 (Cleanup)

: 최종 제출이 끝나면 drafts에 남아 있는 데이터는 더 이상 필요 없습니다. 관리 관점에서도 불필요한 데이터가 쌓이는 것을 막아야 합니다. 그래서 제출 성공 이후 deleteDraft를 호출하도록 설계했습니다.

code
if (result.success) {
  await deleteDraft(userId);
}

이로써 초안은 필요한 순간에만 존재하고, 서비스는 항상 정돈된 상태를 유지합니다.

4) 4단계: 사용자가 원하면 직접 저장하도록 — “임시 저장” 버튼

자동 저장이 아무리 잘 되어 있어도 사용자는 눈앞에서 “저장됨”이라는 명시적인 액션을 보고 싶어합니다.

그래서 작성 UI에 작은 ‘임시 저장’ 버튼을 넣었습니다.

  • 자동 저장: 조용히 백그라운드에서

  • 임시 저장 버튼: 즉시 반응하고 피드백 제공

이 조합은 사용자에게 강한 심리적 안정감을 주었습니다.

| 결론: 좋은 기능은 ‘조용히’ 사용자 시간을 지킨다

자동 저장 기능은 단순히 있어도 되고 없어도 되는 부가 기능이 아닙니다.

✔ 사용자의 시간을 아끼고

✔ 사용자의 노력을 보호하며

✔ 서비스에 대한 신뢰를 쌓는 기능입니다.

이번 구현 과정에서

  • debounce 기반의 최적화,

  • Firestore를 활용한 안정적인 데이터 저장,

  • 사용자 경험에 집중한 흐름 설계,

이 세 가지 요소가 합쳐져 “사용자가 의식하지 않아도 자연스럽게 작동하는 기능”을 만들 수 있었습니다.

좋은 기능은 언제나 자연스럽고 조용합니다. 사용자가 “아, 자동 저장 때문에 살았다”라고 느끼는 그 순간, 서비스는 한 단계 더 신뢰받는 도구가 됩니다.