[Problem] 요구사항 정리
React에서 폼을 구현하는 방법에 대해서 적용한 것들을 정리한 글입니다.
우선 저희 프로젝트의 폼 디자인입니다.

처음에는 form 페이지 하나에서 step 별로 컴포넌트를 나눠서 이전, 다음 버튼을 누르면 그때의 step의 컴포넌트를 렌더링하도록 구현했습니다.
따라서 중간에 step 1 ~ step 4는 페이지가 아니라 컴포넌트 였습니다.
이때는 상태관리에 대해 아무런 계획도 세우지 않은 채 겉모습만 똑같이 만드느라 급급했던 것 같네요..
이후 디자인 팀과 1차 QA를 진행했는데, 아래와 같은 요구사항이 나왔습니다.
✅ 디자인팀에서 준 요구사항
- 위시풀 Progress바의 단계 레이블을 누르면 해당 단계로 화면이 이동했으면 좋겠어요
- 위시풀 만들기 ‘최종점검’ 에서 ‘수정하기’ 버튼 탭할 경우, 이전 단계(마감일)가 아닌, 첫 단계(생일자)로 넘어가요!
- ‘수정하기’ 탭할 경우, 이전에 입력해둔 텍스트들이 사라지지 않고 유지되는게 가능할까요?
- 위시풀 만들 때 필수입력을 다 하지 않으면 ‘다음’ 버튼 비활성화 필요해요.
Solution 1. 멀티스텝 폼으로 구현하자!
2. 위시풀 만들기 ‘최종점검’ 에서 ‘수정하기’ 버튼 탭할 경우, 이전 단계(마감일)가 아닌, 첫 단계(생일자)로 넘어가요!
요구사항 2번을 보고 step의 컴포넌트를 페이지로 바꿔서 멀티 스텝 폼으로 구현해야 겠다고 생각했습니다.
그래야 수정하기 버튼을 눌렀을 때, step1 이 아닌 step4(마감일) 로 이동할 수 있고,
페이지 새로고침 시에도 각 step 이 유지될 수 있기 때문에 결정했습니다.
물론 컴포넌트로 조건처리를 해줘도 가능할 것 같지만, 너무 복잡할 것 같아서 깔끔하게 멀티스텝으로 하기로 했습니다.
Solution 2. 폼 데이터는 session storage 로 관리해서 새로고침, step 별 이동 때도 유지하자
3. ‘수정하기’ 탭할 경우, 이전에 입력해둔 텍스트들이 사라지지 않고 유지되는게 가능할까요?
멀티 스텝 폼의 가장 큰 고민거리는 폼 데이터 관리를 어떻게 할 것인가 였습니다.
각 step 페이지에서 데이터를 받아서 최종 점검 페이지에서 데이터를 뿌려주고, 완료하기 버튼을 눌렀을 때 서버로 데이터가 전달돼야 합니다.
따라서 어딘가에 데이터를 저장해두고, 새로고침해도 폼 데이터를 유지하기 위해 아래와 같은 후보를 생각했습니다.
- Local Storage 또는 Session Storage 를 사용해서 데이터를 저장해두자
- Cookie를 사용하자
Zustand 로 상태관리 라이브러리를 사용하자React hook form을 사용한다(최후 수단)
Zustand로 전역 상태관리를 한다고 해도, 새로고침하면 다시 렌더링되면서 상태값이 초기화 되기 때문에 문제는 해결되지 않습니다.
React hook form 라이브러리는 일단 최대한 쓰지 말고, 정말 필요할 때 써야겠다고 생각했습니다.
나머지 세 가지 옵션을 비교해보겠습니다.
2-1. Local Storage VS Session Storage VS Cookie
Local Storage 와 Session Storage 는 데이터를 키-값 쌍으로 저장할 수 있으며, 비슷한 개념이지만 영구성의 차이입니다.
자세한 내용은 아래 자료를 참고하면 좋을 것 같습니다.
https://ko.javascript.info/localstorage
localStorage와 sessionStorage
ko.javascript.info
이건 위에 자료를 쉽게 풀어서 설명한 글입니다.
https://www.zerocho.com/category/HTML&DOM/post/5918515b1ed39f00182d3048
(HTML&DOM) 로컬스토리지, 세션스토리지 - 그리고 쿠키
안녕하세요. 이번 시간에는 로컬 스토리지(localStorage)와 세션 스토리지(sessionStorage)에 대해 알아보겠습니다. 이름만 봐도 각각의 기능이 뭔지 대충 알겠죠? 영어에 약하신 분들을 위해 간단히 설
www.zerocho.com
간단히 말하면, Local Storage 는 데이터가 브라우저에 영구적으로 저장되고, Session Storage 는 세션(탭) 단위로 데이터가 유지됩니다.
Session Storage 에서 세션 단위라는건, localhost:3000 을 각각 다른 탭에서 띄웠을 때 데이터가 유지되지 않는다는 의미입니다.
Cookie 는 서버에게 누가 보낸 데이터인지를 포함해서 전달되고, 위에 스토리지보다 용량이 매우 작습니다.
저희는 사용자가 위시풀을 생성할 때, 폼을 채우는 과정에서만 데이터가 유지되어야 하니까 session storage가 적당하다고 판단해서 적용했습니다.
Solution 3. 부모 컴포넌트에서 폼 데이터의 변동 사항을 관리하자. [제어 컴포넌트]
4. 위시풀 만들 때 필수입력을 다 하지 않으면 ‘다음’ 버튼 비활성화 필요해요.
저의 코드 구조는 부모 컴포넌트 (step1 ~step4) 안에 Input 공통 컴포넌트가 있는 구조입니다.
그리고 부모 컴포넌트에는 step의 단계 이동을 담당하는 ProgressBar 와 이전/ 다음 버튼인 ButtonContainer 가 있습니다.
| App 폴더 안 step1 ~step4 페이지 | 공통 Input 컴포넌트 |
![]() |
![]() |
const Step3Page = () => {
const step = STEPS.STEP3;
const next = PATH.WISHPOOL_CREATE_STEP4;
return (
<>
<ProgressBar currentStep={step} />
{/* 폼 내용 + input 컴포넌트 */}
<ButtonContainer isNextDisabled={false} next={next} />
</>
);
};
export default Step3Page;
처음에는 자식 컴포넌트(input 컴포넌트)에서 사용자 입력을 sessionStorage.setItem() 하고,
부모 컴포넌트(각 Step 페이지) 에서 useEffect 로 sessionStorage.getItem() 했을 때 값이 없으면 isNextDisabled 를 false로 넘겨줬습니다.
이때의 문제점은 사용자 입력이 부모 컴포넌트에서 실시간으로 반영되지 않아 버튼 비활성화가 제대로 동작하지 않았습니다.
공통 input 컴포넌트는 입력값이 바뀌는 즉시 반영하지만, 부모 컴포넌트에서는 변동사항은 모르고, 최종값만 받기 때문에 이런 문제가 생겼습니다.
하지만 각 단계 이동과 필수입력 체크를 부모 컴포넌트에서 하기 때문에, 사용자 입력값을 공통 Input에서 부모 컴포넌트로 올려줘야 합니다.
그래야 값이 있는지 없는지 필수 입력을 체크하고, 버튼 비활성화 여부를 선택하기 때문입니다.
따라서 자식 컴포넌트에서 onChange로 값이 변동될때마다 부모 컴포넌트로 name과 value (키-값) 를 줬고, 부모에서는 이를 받아 setFormData( [name]: value) 로 실시간으로 입력값을 알 수 있도록 했습니다.
부모 컴포넌트 ( 각 Step 페이지)
// step1Page.tsx
const [formData, setFormData] = useState({ celebrant: '', birthDay: '' });
useEffect(() => {
const initialCelebrant = sessionStorage.getItem('wishpool_celebrant') || '';
const initialBirthDay = sessionStorage.getItem('wishpool_birthDay') || '';
setFormData({
celebrant: initialCelebrant,
birthDay: initialBirthDay,
});
}, []);
const handleInputChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }));
};
const isNextDisabled = !formData.celebrant || !formData.birthDay;
<BaseInput
name="celebrant"
placeholder="생일자의 이름을 알려 주세요."
maxLength={20}
value={formData.celebrant}
onChange={handleInputChange}
/>
자식 컴포넌트 (Input 컴포넌트)
// BaseInput.tsx
const BaseInput = ({
name,
placeholder,
maxLength,
value,
onChange,
}: BaseInputProps) => {
const limit = typeof maxLength === 'number' ? maxLength : undefined;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const text = limit ? e.target.value.slice(0, limit) : e.target.value;
onChange(name, text); // 부모 컴포넌트로 변동 사항 전달
sessionStorage.setItem(`wishpool_${name}`, text);
};
<input
name={name}
type="text"
value={currentVal}
onChange={handleChange}
...
/>
흐름은 이렇습니다!
사용자 입력
↓
BaseInput(handleChange)
↓
onChange(name, value)
↓
Step1Page(handleInputChange)
↓
setFormData → 상태 업데이트
↓
formData 값 변경 → 리렌더링
↓
BaseInput(value=새로운 formData[name])
↓
화면 업데이트 완료 ✅
RHF 을 쓰지 않고도 요구사항에 맞게 구현하긴 했습니다! 폼 내용에 대한 검증은 필수 체크 말고는 필요가 없다고 판단하여 하진 않았습니다.
또 제가 모르는 폼을 구현하는 더 좋은 방법이 많을 것 같습니다..
혹시 이 글을 읽으시고 더 좋은 방법이 있다면 꼭 답글 남겨주세요 !!
읽어주셔서 감사합니다 😊
0. 들어가며
사이드 프로젝트에서 React, Typescript, Nextjs 를 사용하며 폼을 구현하는데에 많은 고민이 있었다.
고민의 흐름은 이렇다.
1. 우리는 왜 Nextjs를 사용했는가? (갑자기 프로젝트의 시작에 대해 고민함.. )
2. 현재 나는 폼을 구현해야 하는데, Nextjs에서는 폼을 어떻게 구현하라고 할까
(Nextjs가 서버를 다룰 수 있다고 해서 쓰는거같은데 우리는 백엔드가 서버를 관리함.)
3. 애초에 폼을 구현하기 위해 알아야 할 너무 많은 부분을 모르고 있음.
결과적으로 React에서도 폼을 구현하는 방법에 대해 너무 모르고 있다고 생각이 들었다.
따라서 Nextjs는 잠깐 치워두고 React에서 폼을 구현하는 방법에 대해서 내가 적용한 것들을 정리해본다.
'FE' 카테고리의 다른 글
| 교회 대학부 주보 웹앱, ‘멍에’ 사용성 테스트(Usability Test) 진행 후기 (0) | 2026.03.09 |
|---|

