9.1 긴 폼, 왜 잘라서 보여줄까요?

회원가입 화면에 입력칸이 스무 개쯤 줄줄이 있다고 해볼게요. 사용자는 스크롤을 내리다 지쳐서 절반도 못 채우고 창을 닫아버려요. 그래서 실무에서는 긴 폼을 여러 단계로 쪼개서 한 번에 서너 개씩만 물어봐요. 이렇게 나눈 폼을 다단계 폼(multi-step form, 여러 단계로 나눈 폼) 또는 위저드 폼(wizard form, 마법사식 폼)이라고 불러요.


한 단계에 보이는 입력이 적으면 사용자는 부담을 덜 느껴요. "이번엔 이름이랑 이메일만 적으면 되네" 싶으니까 술술 넘어가죠. 오늘은 이 다단계 폼을 상태 유지, 단계별 검증, 뒤로가기까지 직접 만들어볼게요.

9.2 화면은 어떻게 나누죠?

가장 흔한 방식은 form은 하나만 두고, 그 안에서 단계별로 묶음(div)을 만들어 지금 단계만 보여주고 나머지는 숨기는 거예요. 폼을 여러 개로 쪼개지 않는 게 핵심이에요. 그래야 마지막에 값을 한 번에 모아 보낼 수 있거든요.


<form id="signup">
<div class="step" data-step="1">
<label>이름 <input name="name"></label>
</div>
<div class="step" data-step="2" hidden>
<label>이메일 <input name="email" type="email"></label>
</div>
<div class="step" data-step="3" hidden>
<label>비밀번호 <input name="pw" type="password"></label>
</div>
<button type="button" id="prev">이전</button>
<button type="button" id="next">다음</button>
</form>


여기서 hidden은 그 요소를 화면에서 안 보이게 감추는 HTML 속성이에요. 2단계와 3단계에 붙여뒀으니 처음엔 1단계만 보여요. 다음·이전 버튼은 type="button"으로 뒀는데, 이걸 안 붙이면 버튼을 누를 때마다 폼이 곧장 제출돼버려요. 단계를 넘기는 버튼은 제출 버튼이 아니라는 걸 분명히 해주는 거예요.

9.3 다음 단계로 어떻게 넘겨요?

현재 몇 번째 단계인지를 변수 하나로 기억해요. 다음을 누르면 숫자를 올리고, 이전을 누르면 내려요. 그리고 그 숫자에 맞는 단계만 보여주는 함수를 하나 만들어요.


const steps = document.querySelectorAll(".step");
let current = 1;

function show(n) {
steps.forEach(s => {
s.hidden = Number(s.dataset.step) !== n;
});
}

document.getElementById("next").onclick = () => {
if (current < steps.length) { current++; show(current); }
};
document.getElementById("prev").onclick = () => {
if (current > 1) { current--; show(current); }
};


동작을 볼게요. 처음엔 1단계만 떠 있어요. 다음을 누르면 current가 2가 되고 show(2)가 돌면서 2단계만 남고 나머지는 hidden이 붙어요. 이전을 누르면 다시 1로 내려가요. dataset.step은 아까 HTML에 적어둔 data-step 값을 읽어오는 부분이에요. 그 숫자가 지금 단계와 다르면 숨기고, 같으면 보여주는 거죠.

9.4 뒤로 가면 입력한 게 사라지나요?

여기가 다단계 폼에서 사람들이 가장 많이 헷갈리는 부분이에요. 결론부터 말하면 안 사라져요. 우리는 단계를 숨기기만 했지 지운 게 아니거든요. input은 화면에서 안 보여도 DOM에 그대로 살아 있어요. 그래서 2단계에서 이메일을 적고 1단계로 돌아갔다가 다시 2단계로 오면 이메일이 그대로 남아 있어요.


만약 단계마다 페이지를 따로 만들어서 이동했다면 이야기가 완전히 달라져요. 페이지가 바뀌면 앞 페이지의 입력은 날아가니까, 값을 어딘가에 저장했다가 다시 채워 넣어야 하죠. 한 폼 안에서 숨기고 보이는 방식은 그 번거로움이 아예 없다는 게 큰 장점이에요.

9.5 단계마다 검증은 어떻게 하죠?

다음으로 넘어가기 전에 지금 단계의 입력이 제대로 됐는지 확인해야 해요. 안 그러면 이름을 비워둔 채 끝 단계까지 갔다가 제출할 때 한꺼번에 빨간 줄이 뜨거든요. 브라우저에는 입력이 규칙에 맞는지 검사해주는 checkValidity(유효성 검사) 기능이 있어요. 지금 단계 안의 input들만 골라서 하나씩 검사해요.


function stepValid(n) {
const box = document.querySelector('.step[data-step="' + n + '"]');
const fields = box.querySelectorAll("input");
for (const f of fields) {
if (!f.checkValidity()) {
f.reportValidity();
return false;
}
}
return true;
}


이제 다음 버튼에서 이 함수를 먼저 불러요. 통과했을 때만 단계를 올리는 거죠.


document.getElementById("next").onclick = () => {
if (!stepValid(current)) return;
if (current < steps.length) { current++; show(current); }
};


reportValidity는 어디가 잘못됐는지 그 칸 옆에 말풍선으로 알려주는 기능이에요. 예를 들어 이메일 칸에 골뱅이 없이 아무 글자나 넣고 다음을 누르면 "이메일 주소에 @를 포함하세요" 같은 안내가 뜨면서 넘어가지 않아요. 이전 버튼에는 이 검사를 걸지 않아요. 뒤로 가는 건 언제든 자유롭게 두는 게 사용자 입장에서 편하거든요.

9.6 마지막엔 어떻게 한 번에 보내죠?

마지막 단계에서는 다음 버튼 대신 제출 버튼을 보여줘요. 값은 한 폼 안에 다 모여 있으니, 제출할 때 FormData(폼 데이터, 입력값을 한 번에 담는 객체)로 통째로 긁어오면 돼요.


document.getElementById("signup").onsubmit = (e) => {
e.preventDefault();
if (!stepValid(current)) return;
const data = new FormData(e.target);
console.log(data.get("name"), data.get("email"));
};


e.preventDefault()는 폼이 기본 동작대로 페이지를 새로고침하며 보내는 걸 막는 부분이에요. 이걸 넣어야 자바스크립트가 값을 가로채서 원하는 대로 처리할 수 있어요. 숨겨진 단계의 input도 FormData에 전부 담겨요. 화면에 안 보인다고 값이 빠지는 게 아니라는 거, 이 대목에서 다시 확인되죠.

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

다단계 폼은 긴 폼을 여러 단계로 쪼개 사용자 부담을 줄이는 방법이었어요. 핵심은 form은 하나만 두고 단계별 묶음을 hidden으로 숨겼다 보였다 하는 거였죠. 이러면 뒤로 가도 입력이 DOM에 그대로 남아 값을 다시 채울 필요가 없었어요.


넘어가기 전에는 checkValidity로 그 단계만 검사하고, 마지막엔 FormData로 값을 한 번에 모아 보냈고요. 세 단계짜리 폼을 직접 만들어서 다음·이전을 눌러보고, 이메일을 비운 채 넘겨보세요. 숨긴 값이 살아 있는 걸 눈으로 보면 이 구조가 왜 편한지 바로 와닿을 거예요.