글을 한참 써놨는데 브라우저가 튕기거나 실수로 탭을 닫아서 다 날려본 경험, 다들 있으시죠? 에디터를 만드는 입장이 되면 이건 남의 일이 아니에요. 사용자가 정성껏 쓴 글이 한 방에 사라지면 그 서비스는 두 번 다시 안 써요. 그래서 에디터에서 저장복원은 화면을 예쁘게 만드는 것보다 먼저 챙겨야 하는 뼈대예요. 오늘은 타이핑을 자동으로 저장하고, 브라우저가 꺼져도 초안을 지키고, 서버에 안전하게 올리는 흐름을 하나씩 만들어 볼게요.

26.1 저장 버튼만 있으면 되는 거 아닌가요?

가장 단순한 에디터는 저장 버튼 하나예요. 다 쓰고 버튼을 누르면 서버로 보내는 방식이죠. 문제는 사람들이 버튼을 안 누른다는 거예요. 글을 쓰다 전화가 오고, 다른 탭을 열고, 배터리가 나가요. 그 사이에 쓴 글은 아무 데도 저장이 안 돼 있어요.


그래서 요즘 에디터는 사용자가 신경 쓰지 않아도 알아서 저장돼요. 구글 문서나 노션에서 저장 버튼을 눌러본 기억이 없죠? 화면 구석에 "저장됨"이 조용히 떠 있을 뿐이에요. 우리가 만들 것도 이거예요. 큰 그림은 세 겹이에요. 첫째, 타이핑을 자동으로 감지해 저장을 걸고. 둘째, 브라우저 안(로컬)에 초안을 남겨 갑작스러운 사고를 막고. 셋째, 안정적으로 서버에 올려 어느 기기에서나 이어 쓰게 하는 거예요.

26.2 타이핑할 때마다 저장하면 안 되나요?

가장 먼저 떠오르는 건 "글자가 바뀔 때마다 저장하자"예요. 에디터의 input(입력) 이벤트에 저장 함수를 붙이면 되겠죠. 그런데 이러면 큰일 나요. 1초에 대여섯 글자를 치는데 글자마다 서버로 요청을 날리면 서버가 요청 폭탄을 맞아요.


그래서 쓰는 게 디바운스(debounce, 잠잠해질 때까지 기다렸다 한 번만 실행)예요. 타이핑이 멈춘 뒤 일정 시간이 지나면 그때 딱 한 번 저장하는 거죠.


let timer;
editor.addEventListener("input", function () {
clearTimeout(timer);
timer = setTimeout(function () {
save(editor.innerHTML);
}, 800);
});


동작을 따라가 볼게요. 글자를 칠 때마다 input 이벤트가 터지고, 그때마다 clearTimeout으로 이전에 걸어둔 예약을 지워요. 그리고 setTimeout으로 "800밀리초(0.8초) 뒤에 저장해줘"를 새로 예약하죠. 계속 치는 동안엔 예약이 지워지고 다시 걸리니 저장이 안 돼요. 그러다 손을 0.8초 멈추는 순간 예약이 살아남아 save가 한 번 실행돼요. 결과적으로 열 문장을 쭉 쳐도 저장은 손 멈출 때 한두 번만 나가요. 시간은 보통 0.5초에서 1초 사이로 잡아요. 너무 짧으면 요청이 잦고, 너무 길면 사고 났을 때 잃는 양이 많아지니까요.

26.3 브라우저가 갑자기 꺼지면요?

디바운스로 저장을 걸어도, 서버로 보내기 직전에 브라우저가 꺼지면요? 아직 안 나간 0.8초어치는 사라져요. 인터넷이 끊긴 상태라면 서버 저장 자체가 실패하고요. 그래서 서버로 가기 전에 브라우저 안에 먼저 한 부를 복사해둬요. 이때 쓰는 게 localStorage(로컬 스토리지, 브라우저에 데이터를 남겨두는 저장소)예요.


timer = setTimeout(function () {
localStorage.setItem("draft", editor.innerHTML);
save(editor.innerHTML);
}, 800);


localStorage.setItem에 "draft"(초안)라는 이름표와 함께 지금 글을 넣으면, 이 데이터는 브라우저를 껐다 켜도 남아 있어요. 서버 요청과 달리 인터넷이 필요 없고 즉시 저장돼서 순식간에 끝나요. 그래서 서버 저장이 실패하든 브라우저가 튕기든, 최소한 이 기기 안에는 마지막 글이 남죠. localStorage는 글자(문자열)만 담을 수 있어서, 데이터가 여러 갈래면 JSON.stringify로 문자열로 바꿔 넣는다는 점만 기억해두세요.

26.4 나갈 때 경고를 띄우고 싶어요

저장이 아직 안 끝났는데 사용자가 탭을 닫으려 하면, "저장 안 됐어요, 정말 나갈래요?"하고 물어보고 싶죠. 브라우저는 이걸 위한 beforeunload(떠나기 직전) 이벤트를 줘요.


window.addEventListener("beforeunload", function (e) {
if (dirty) {
e.preventDefault();
e.returnValue = "";
}
});


여기서 dirty(더티, 저장 안 된 변경이 있음)는 우리가 직접 관리하는 표시예요. 글이 바뀌면 true로 켜고, 서버 저장이 끝나면 false로 끄는 식이죠. 저장 안 된 게 있을 때 e.preventDefault를 부르면 브라우저가 "이 사이트를 떠나시겠습니까?" 확인 창을 대신 띄워줘요. 주의할 점은 이 창의 문구를 우리 마음대로 못 바꾼다는 거예요. 보안 때문에 브라우저가 정해진 문구만 보여주거든요. 그래도 실수로 닫는 사고는 이걸로 꽤 막아요.

26.5 다시 들어왔을 때 이어 쓰려면요?

저장을 아무리 잘해도 복원이 없으면 반쪽이에요. 사용자가 사고로 나갔다 다시 들어왔을 때, 브라우저에 남겨둔 초안을 찾아 되살려줘야죠. 에디터가 처음 뜰 때 이렇게 확인해요.


const draft = localStorage.getItem("draft");
if (draft) {
if (confirm("작성 중이던 글이 있어요. 이어서 쓸까요?")) {
editor.innerHTML = draft;
}
}


localStorage.getItem으로 아까 저장해둔 초안을 꺼내와요. 값이 있으면 사용자에게 물어보고, 그렇다고 하면 에디터 내용에 도로 채워 넣죠. 여기서 바로 덮어쓰지 않고 물어보는 게 중요해요. 서버에 이미 최신 글이 있는데 옛날 초안으로 무턱대고 덮어버리면 오히려 사고거든요. 그리고 서버 저장이 확실히 끝난 뒤에는 localStorage.removeItem("draft")로 초안을 지워줘요. 안 지우면 다음에 새 글을 쓸 때 옛날 초안이 튀어나와 사용자가 당황해요. 저는 이 청소를 빼먹어서 "왜 새 글 쓰는데 예전 글이 떠요?"라는 문의를 받은 적이 있어요.

26.6 서버 저장은 언제, 어떻게 하죠?

로컬 초안은 이 기기만 지켜줘요. 회사 PC에서 쓰던 글을 집 노트북에서 이어 쓰려면 결국 서버에 있어야죠. 서버 저장도 디바운스로 묶어서, 손을 멈출 때마다 조용히 올려요.


function save(html) {
fetch("/api/posts/123", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: html })
});
}


fetch로 서버의 글 주소에 PUT(수정 요청)을 보내요. 글 내용을 JSON.stringify로 문자열로 바꿔 몸통에 실어 보내면 서버가 받아 데이터베이스를 갱신하죠. 여기서 실무 팁 하나. 저장이 성공했는지를 사용자에게 알려줘야 해요. 성공하면 화면 구석에 "저장됨", 실패하면 "저장 실패, 다시 시도 중"처럼 상태를 보여주는 거죠. 이 작은 표시 하나가 사용자에게 안심을 줘요.

26.7 두 곳에서 동시에 고치면요?

여기서 골치 아픈 상황이 하나 나와요. 사용자가 회사 PC와 집 노트북 두 곳에서 같은 글을 열어놓고 각각 고치면요? 나중에 저장한 쪽이 앞의 것을 덮어써서 한쪽 수정이 통째로 사라져요. 이걸 충돌(conflict)이라고 해요.


여럿이 같은 글을 실시간으로 고치는 완전한 해결은 그 자체로 큰 주제라, 여기선 가장 기본적인 안전장치만 볼게요. 글마다 버전 번호를 붙이는 방법이에요. 서버는 저장할 때마다 버전을 1씩 올리고, 에디터는 저장 요청에 내가 알고 있는 버전을 같이 실어 보내죠.


body: JSON.stringify({ content: html, version: 7 })


서버는 이 version이 자기가 가진 최신 버전과 같은지 봐요. 같으면 정상 저장하고 버전을 8로 올려요. 그런데 다른 기기가 먼저 저장해서 서버 버전이 이미 8이 됐다면, 내가 보낸 7은 낡은 것이죠. 이때 서버는 저장을 거부하고 409(Conflict, 충돌)라는 신호를 돌려줘요. 에디터는 이 신호를 받으면 사용자에게 "다른 곳에서 먼저 수정됐어요. 최신 글을 불러올까요?"라고 알려주면 되고요. 무작정 덮어쓰는 것보다 훨씬 안전하죠.

26.8 오늘 정리

에디터의 저장은 여러 겹의 안전망이라는 걸 봤어요.
1. 타이핑은 디바운스로 묶어, 손 멈출 때 한 번만 저장해 서버 부담을 줄여요.
2. 서버로 보내기 전에 localStorage에 초안을 남겨, 브라우저가 꺼져도 이 기기엔 글이 살아 있게 해요.
3. 다시 들어오면 초안을 찾아 물어보고 복원하고, 서버 저장이 끝나면 초안을 지워요.
4. beforeunload로 저장 안 된 채 나가는 실수를 막아요.
5. 버전 번호로 두 기기의 충돌을 감지해, 남의 수정을 조용히 덮어쓰는 사고를 피해요.


화려한 기능은 아니지만, 사용자가 글을 잃지 않는다는 이 신뢰가 에디터의 진짜 바탕이에요. 저장과 복원이 탄탄하면, 그 위에 어떤 화려한 기능을 얹어도 사용자는 마음 놓고 글에 집중할 수 있어요.