11.1 폼에 상태가 있다는 게 무슨 말이에요?

폼은 그냥 입력칸 모음 같지만, 사실 지금 어떤 상태인지를 늘 품고 있어요. 사용자가 아무것도 안 건드린 깨끗한 상태(pristine, 처음 그대로)와, 한 글자라도 고친 변경된 상태(dirty, 손댄 상태)로 나뉘어요. 이 둘을 구분하는 게 오늘의 핵심이에요.


왜 중요하냐면요, "저장 안 했는데 나가시겠어요?" 같은 안내를 띄우려면 사용자가 뭔가 고쳤는지를 알아야 하거든요. 아무것도 안 건드렸는데 경고를 띄우면 성가시기만 하죠. 오늘은 폼을 처음으로 되돌리는 초기화, 손댔는지 알아채는 변경 감지, 그리고 이탈 경고까지 만들어볼게요.

11.2 폼을 처음 상태로 되돌리려면요?

"다시 입력" 버튼을 눌렀을 때 모든 칸을 처음으로 돌리고 싶다고 해볼게요. 칸을 하나씩 지울 필요 없어요. 폼에는 reset(리셋, 초기화) 기능이 있어서 한 방에 처음 상태로 돌려줘요.


<form id="profile">
<input name="nick" value="기본닉네임">
<input name="bio">
<button type="button" id="clear">다시 입력</button>
</form>


document.getElementById("clear").onclick = () => {
document.getElementById("profile").reset();
};


여기서 오해하기 쉬운 게 있어요. reset은 칸을 텅 비우는 게 아니라, 처음 값으로 되돌리는 거예요. 위에서 nick 칸은 value로 "기본닉네임"을 갖고 시작했죠? reset을 누르면 이 칸은 비는 게 아니라 다시 "기본닉네임"으로 돌아가요. 반대로 bio 칸은 처음이 비어 있었으니 비워지고요. 처음 그 모습으로 되감는다고 기억하면 딱 맞아요.


실무 팁을 하나 더할게요. 다시 입력 버튼은 사용자가 애써 채운 걸 한 번에 날려버려요. 그래서 누르자마자 곧장 reset하기보다 "정말 지울까요?" 하고 한 번 물어본 뒤 실행하는 게 안전해요. 실수로 스친 클릭 한 번에 십 분 걸려 쓴 내용이 사라지면 사용자는 크게 화가 나거든요. 되돌리기 힘든 동작일수록 확인을 한 번 끼우는 습관이 좋아요.

11.3 사용자가 뭘 고쳤는지 어떻게 알죠?

손을 댔는지 알아내는 가장 쉬운 방법은 깃발 하나를 세우는 거예요. 처음엔 안 고쳤다고 표시해두고, 어느 칸이든 값이 바뀌면 그 깃발을 올려요. 폼 전체에 input 사건(값이 바뀔 때마다 일어나는 신호)을 한 번만 걸면 안쪽 모든 칸을 커버해요.


let dirty = false;
const form = document.getElementById("profile");

form.addEventListener("input", () => {
dirty = true;
});


이제 사용자가 어느 칸이든 한 글자만 쳐도 dirty가 true로 바뀌어요. 이 깃발 하나면 "지금 저장 안 된 변경이 있는가"를 언제든 물어볼 수 있죠. 칸마다 따로 감시할 필요 없이, 폼에 input을 한 번 거는 것으로 충분해요. 안쪽 칸에서 일어난 신호가 바깥 폼까지 올라오는 성질을 이용한 거예요.


더 정밀하게 하고 싶으면 처음 값들을 저장해뒀다가 현재 값과 비교하는 방법도 있어요. 이러면 고쳤다가 원래 값으로 되돌린 경우는 안 고친 걸로 칠 수 있죠. 처음 값은 FormData로 통째로 저장해두고, 검사할 때 현재 값과 하나씩 비교하면 돼요.


const initial = new FormData(form);

function isDirty() {
const now = new FormData(form);
for (const [key, value] of now) {
if (initial.get(key) !== value) return true;
}
return false;
}


isDirty는 지금 값을 처음 값과 하나하나 맞대봐요. 하나라도 다르면 true, 전부 같으면 false죠. 그래서 사용자가 이름을 고쳤다가 다시 원래대로 되돌려 놓으면 false가 나와요. 깃발 방식은 이 경우에도 true로 남는다는 차이가 있죠. 다만 대부분의 실무에선 깃발 방식으로 충분하고, 정말 세밀한 판단이 필요할 때만 이 비교 방식을 써요.

11.4 실수로 창을 닫으면 어떡하죠?

긴 글을 다 써놨는데 실수로 탭을 닫으면 아찔하죠. 브라우저에는 페이지를 떠나기 직전에 일어나는 beforeunload(비포언로드, 이탈 직전) 신호가 있어요. 여기서 앞서 만든 dirty 깃발을 확인해, 고친 게 있을 때만 경고를 띄워요.


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


동작을 볼게요. 칸을 하나라도 고친 뒤 탭을 닫거나 새로고침하면 "이 사이트를 떠나시겠습니까? 변경사항이 저장되지 않을 수 있습니다" 같은 확인창이 떠요. 아무것도 안 고쳤으면 dirty가 false라 조용히 그냥 닫히고요.


한 가지 알아둘 게 있어요. 이 확인창의 문구는 우리가 마음대로 못 바꿔요. 브라우저가 정해진 문장을 보여줘요. 옛날에 이걸 악용한 사이트가 많아서, 지금은 e.returnValue에 무슨 글자를 넣든 브라우저 기본 문구만 떠요. 우리가 정할 수 있는 건 "경고를 띄울지 말지"까지예요.


또 하나 알아둘 점이 있어요. beforeunload는 탭을 닫거나 주소를 바꿔 페이지 자체를 떠날 때 일어나요. 그래서 화면 안에서 버튼만 눌러 이동하는 방식이라면 이 신호가 안 뜰 수도 있어요. 그런 경우엔 이동 처리를 하기 직전에 직접 dirty를 확인해 "저장 안 됐는데 이동할까요?"를 물어봐야 해요. beforeunload는 진짜 이탈을 막는 안전망 정도로 생각하면 돼요.

11.5 저장한 뒤엔 경고를 꺼야겠죠?

여기서 놓치기 쉬운 함정이 있어요. 사용자가 저장을 눌렀는데도 경고가 뜨는 경우예요. 저장은 됐는데 dirty 깃발을 안 내려서 그래요. 제출이 성공하면 깃발을 다시 false로 내려줘야 해요.


form.addEventListener("submit", (e) => {
e.preventDefault();
// 서버에 저장하는 처리...
dirty = false;
});


이렇게 제출 직후 dirty를 내리면, 저장을 마치고 페이지를 벗어날 때 beforeunload가 깃발이 false인 걸 보고 경고를 안 띄워요. 저장 안 했을 땐 붙잡고, 저장했을 땐 순순히 보내주는 거죠. "다시 입력" 버튼을 눌러 reset한 직후에도 마찬가지로 dirty를 false로 맞춰주면 상태가 깔끔하게 맞아요.

11.6 오늘 배운 걸 정리해볼게요

폼에는 안 건드린 깨끗한 상태와 손댄 변경 상태가 있었어요. form.reset()은 칸을 비우는 게 아니라 처음 값으로 되돌리는 기능이었고요. 사용자가 고쳤는지는 input 사건으로 dirty 깃발 하나만 올리면 간단히 알 수 있었죠.


그 깃발을 beforeunload에서 확인해, 저장 안 한 변경이 있을 때만 이탈 경고를 띄웠어요. 대신 저장이나 초기화를 마치면 깃발을 다시 내려 엉뚱한 경고를 막았고요. 직접 폼을 하나 만들어서 한 글자 친 뒤 탭을 닫아보고, 저장을 누른 뒤에도 닫아보세요. 경고가 떴다 안 떴다 하는 걸 보면 상태 관리가 왜 필요한지 몸으로 알게 될 거예요.