FE
[SvelteKit] Svelte + Firestore로 구현한 ‘자동 저장’ 기능 구축기
| “저장 버튼을 누르기 전에 브라우저가 닫혔습니다.”
안녕하세요 팡일입니다!
혹시 이런 경험 있으신가요? 상상만 해도 아찔한 상황입니다. 몇 시간 동안 공들여 작성한 글이 예기치 못한 브라우저 오류나 탭 닫기 한 번에 사라진다면, 누구라도 깊은 허탈감을 느낄 수밖에 없습니다.
그리고 이 경험은 결국 이렇게 이어집니다.
“이 서비스는 내 시간을 존중하지 않는구나.”
사용자 경험에서 이런 감정은 치명적입니다. 그래서 저는 이번 프로젝트에서 자동 저장(auto save) 과 초안(Draft) 기능을 통해 사용자의 시간을 지켜주는 기능을 구현했습니다. 사용자가 글을 작성하는 동안 조용히 뒷단에서 저장해주고, 다시 돌아왔을 때 “이어서 작성하시겠어요?”라고 묻는 친절한 경험을 제공하는 것이 목표였습니다.
이 글에서는 Svelte와 Firestore를 활용해 안정적이고, 빠르고, 유저 친화적인 자동 저장 기능을 실제로 어떻게 구현했는지 그 과정을 공유하겠습니다.
| 목표 정의: 좋은 Draft 시스템이 갖춰야 할 조건
자동 저장은 단순히 ‘저장 한번 해주는 기능’이 아닙니다. 사용자가 진짜로 믿고 쓸 수 있는 기능이어야 했습니다. 그래서 다음과 같은 목표를 세웠습니다.
자동 저장: 사용자가 타이핑을 멈추면 일정 시간 뒤 자동으로 임시 저장한다.
불러오기: 페이지에 다시 방문했을 때 이전 초안을 감지하고 불러올지 선택권을 제공한다.
정리(Cleanup): 사용자가 최종 제출하면 초안은 자동으로 삭제하여 깔끔한 상태를 유지한다.
심리적 안정감: 자동 저장이 있더라도 ‘임시 저장’ 버튼을 제공하여 사용자가 스스로 저장할 수 있는 심리적 여유를 제공한다.
| 구현 과정
1) 1단계: 초안을 저장할 공간 — Firestore의 drafts 컬렉션
자동 저장 기능의 핵심은 “신뢰성 있는 저장 공간”입니다. 저는 Firestore에 drafts라는 전용 컬렉션을 만들고 문서 ID를 userId로 지정했습니다.
/drafts/{userId}이 구조 덕분에 특정 사용자의 초안을 불러오거나 지우는 것이 매우 간단해졌습니다.
서비스 레이어에는 다음 세 가지 기능을 구현했습니다.
// 초안 저장 또는 덮어쓰기
async function saveDraft(userId, draftData) { ... }
// 초안 불러오기
async function getDraft(userId) { ... }
// 초안 삭제
async function deleteDraft(userId) { ... }2) 2단계: 자동 저장의 두뇌 — debounce
문제는 여기서 시작됩니다. onInput마다 저장 함수를 호출해 버리면, 몇 초 만에 Firestore에 수백 번의 요청이 발생합니다. 이는 비용·성능·안정성 측면에서 모두 최악입니다.
그래서 도입한 기술이 바로 debounce입니다.
“연속되는 이벤트 중 마지막 이벤트가 끝난 후 일정 시간이 지나면 단 한 번만 실행한다.”
자동 저장에 완벽히 어울리는 메커니즘입니다.
function debounce(func, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}이제 writeStore의 내용이 변경될 때마다 debounce된 저장 함수를 호출합니다.
<script>
const debouncedSave = debounce(async () => {
await saveDraft(userId, $writeStore);
// "자동 저장되었습니다." 같은 토스트를 띄우면 UX가 더 좋아집니다.
}, 3000);
writeStore.subscribe(debouncedSave);
</script>결과적으로 사용자가 아무리 빠르게 타이핑해도 딱 ‘타이핑을 멈춘 시점 + 3초 뒤’에만 저장이 실행됩니다.
3) 3단계: UX를 완성하는 기능 — 불러오기 & 삭제
(1) 불러오기 (Restore)
: 사용자가 다시 글쓰기 페이지에 진입했을 때 자동 저장된 글이 있다면, 반드시 이를 알려야 합니다.
onMount(async () => {
const draft = await getDraft(userId);
if (draft) {
if (confirm("작성 중이던 글이 있습니다. 이어서 작성하시겠습니까?")) {
hydrateWriteStore(draft);
}
}
});이 한 줄의 confirm 모달이 사용자의 시간을 지켜줍니다.
(2) 삭제 (Cleanup)
: 최종 제출이 끝나면 drafts에 남아 있는 데이터는 더 이상 필요 없습니다. 관리 관점에서도 불필요한 데이터가 쌓이는 것을 막아야 합니다. 그래서 제출 성공 이후 deleteDraft를 호출하도록 설계했습니다.
if (result.success) {
await deleteDraft(userId);
}이로써 초안은 필요한 순간에만 존재하고, 서비스는 항상 정돈된 상태를 유지합니다.
4) 4단계: 사용자가 원하면 직접 저장하도록 — “임시 저장” 버튼
자동 저장이 아무리 잘 되어 있어도 사용자는 눈앞에서 “저장됨”이라는 명시적인 액션을 보고 싶어합니다.
그래서 작성 UI에 작은 ‘임시 저장’ 버튼을 넣었습니다.
자동 저장: 조용히 백그라운드에서
임시 저장 버튼: 즉시 반응하고 피드백 제공
이 조합은 사용자에게 강한 심리적 안정감을 주었습니다.
| 결론: 좋은 기능은 ‘조용히’ 사용자 시간을 지킨다
자동 저장 기능은 단순히 있어도 되고 없어도 되는 부가 기능이 아닙니다.
✔ 사용자의 시간을 아끼고
✔ 사용자의 노력을 보호하며
✔ 서비스에 대한 신뢰를 쌓는 기능입니다.
이번 구현 과정에서
debounce 기반의 최적화,
Firestore를 활용한 안정적인 데이터 저장,
사용자 경험에 집중한 흐름 설계,
이 세 가지 요소가 합쳐져 “사용자가 의식하지 않아도 자연스럽게 작동하는 기능”을 만들 수 있었습니다.
좋은 기능은 언제나 자연스럽고 조용합니다. 사용자가 “아, 자동 저장 때문에 살았다”라고 느끼는 그 순간, 서비스는 한 단계 더 신뢰받는 도구가 됩니다.