안녕하세요, 초원입니다. 👩🏻💻
두 번의 외주 프로젝트를 맡아 개발을 진행해보니, 이제야 작업 흐름이 눈에 들어오기 시작했는데요!
공통 컴포넌트 제작, API 연동 등 .. 중요한 작업이 많지만,
그 중에서도 폼 관리, 상태 관리는 굉장히 까다로운 작업이라고 느끼는 요즘입니다 ..
하다보면 익숙해져서 뭐가 어려웠었는지 다 잊을 것 같아 이제부턴 기록을 잘 남겨보려고 해요. 예 👏🏻 👏🏻 👏🏻
그럼 시작해볼까요 ~
프론트엔드 개발의 세 가지 패턴: 설문조사 패턴
글의 제목은 토스 SLASH23 퍼널: 쏟아지는 페이지 한 방에 관리하기의 한 문장을 인용해 보았어요.
해당 세션에서는 프론트엔드 개발의 대표적인 세 가지 패턴을 소개합니다.
1. 목록 페이지에서 클릭 시 상세 페이지로 이동하는 상점 패턴
2. 하나의 페이지로 관리되는 단일 페이지 앱
3. 단계 별로 페이지 이동 후 최종 페이지에 도달하는 설문조사 패턴
이 중 제가 개발하고 있는 서비스는 설문조사 패턴을 사용하여
단계 별로 폼(form)을 관리하고 최종 페이지에서 제출하는 형태로 진행이 됩니다.
설문조사 패턴



흐름은 다음과 같습니다.
- (왼편) 진행 단계를 확인할 수 있는 ProgressIndicator의 각 단계를 누르면 해당 단계로 이동할 수 있다.
- (하단) BottomStepNavigator에 있는 이전, 다음 버튼을 누르면 한 단계씩 전,후로 이동할 수 있다.
- 마지막 단계에 도달 후 버튼을 누르면 모든 단계에서 작성한 폼이 한 번에 제출된다.
구성은 다음과 같습니다.
- 1단계: 폼 없음
- 2단계: 폼 없음
- 3단계: 폼 3개
- 4단계: 폼 2개 + (조건부 렌더링으로 생성된 추가 폼)
- 5단계: 폼 없음
실제 요구사항은 좀 더 복잡하지만, 이번 글에서는 이 정도만 가져오면 될 것 같네요!
Ant Design Form
아 참, 사용 중인 라이브러리를 소개해 드릴게요.
여러 분은 폼을 어떻게 관리하시나요?
useState와 onChange로 하나하나 관리해줄 수도 있지만,
요즘은 잘 되어 있는 라이브러리가 너무도 많죠 🙆🏻♀️
저번 프로젝트에서는 zod + react-hook-form 조합으로 폼을 관리했는데,
이번엔 Ant Design Form을 사용했습니다. (팀에서 UI 라이브러리로 사용 중이기 때문에~?)
Ant Design Form 컴포넌트 사용하여 폼 관리하기
import { Form, Input } from "antd";
function Parent() {
const [form] = Form.useForm();
return (
<Form form={form}>
<Child1 />
<Child2 />
</Form>
);
}
function Child1() {
return (
<Form.Item name="field1" label="Field 1">
<Input />
</Form.Item>
);
}
function Child2() {
return (
<Form.Item name="field2" label="Field 2">
<Input />
</Form.Item>
);
}
export default Parent;
어떻게 적용할 수 있는지 간단한 예제 코드를 가져와봤어요.
1. 폼 인스턴스 생성
const [form] = Form.useForm();
useForm 훅을 사용하여 폼 인스턴스를 생성하고 이를 Form 컴포넌트에 전달함으로써 폼 상태를 관리할 수 있습니다.
2. Form 컴포넌트
<Form form={form}>
<Child1 />
<Child2 />
</Form>
Form 컴포넌트는 form 인스턴스를 통해 폼 필드의 상태와 유효성을 관리합니다.
이를 통해 전체 폼의 상태를 일관되게 유지할 수 있습니다.
3. 폼 필드 정의:
<Form.Item name="field1" label="Field 1">
<Input />
</Form.Item>
각 필드는 Form.Item으로 정의되며, name 속성으로 폼 필드의 이름을 지정하고 label 속성으로 라벨을 설정합니다.
이 구조를 통해 폼 데이터를 쉽게 관리하고, 각 필드의 상태를 추적할 수 있습니다.
위와 같이 폼 관리를 간편하게 할 수 있으며, 폼 상태와 유효성 검사를 쉽게 처리할 수 있어요!
그래서 구현을 어떻게 했나요?
function Page() {
const [form] = Form.useForm();
const { step, updateStep, formValues } = useStore();
useEffect(() => {
form.setFieldsValue({ ...formValues });
}, []);
const handlePrevButton = () => {
// 이전 단계로 이동
};
const handleNextButton = () => {
// 다음 단계로 이동
};
const onFinish = (values: ValuesType) => {
console.log("Form values: ", values);
};
return (
<>
<ProgressIndicator selectedStep={step} onStepChange={updateStep} />
<StyledForm form={form} onFinish={onFinish}>
<컴포넌트1 />
{step === 1 && <컴포넌트2 />}
{step === 2 && <컴포넌트3 />}
{step === 3 && <컴포넌트4 />}
{step === 4 && <컴포넌트5 />}
<BottomStepNavigator
prev={handlePrevButton}
next={handleNextButton}
/>
</StyledForm>
</>
);
}
export default Page;
처음에는 step의 상태에 따라 조건부 렌더링을 하도록 컴포넌트를 구현하였습니다.
ProgressIndicator 조작에 따라 단계가 잘 이동되고 해당 페이지도 잘 보여졌지만,
문제는 마지막에 제출할 때 formValues를 콘솔로 찍어보니 아무 것도 안담기지 말입니다 ..?
처음엔 form 인스턴스가 props로 잘 전달되지 않아서 발생한 문제인 줄 알고,
폼을 가지고 있는 3, 4 단계에서 form values를 출력해보니 모두 잘 나오는 것이 확인됐습니다.
마지막 페이지에서 확인할 수 없던 것이라면 .. Antd form이 어떤 이유에서 name 값을 인지하지 못하나?
의심을 하였고 아래와 같이 구현 방식을 바꿔보았습니다.
이렇게 바꿨어요
function Page() {
const [form] = Form.useForm();
const { step, formValues } = useStore();
useEffect(() => {
form.setFieldsValue({ ...formValues });
}, []);
const handlePrevButton = () => {
// 이전 단계로 이동
};
const handleNextButton = () => {
// 다음 단계로 이동
};
const onFinish = (values: ValuesType) => {
console.log("Form values: ", values);
};
return (
<>
<ProgressIndicator selectedStep={step} onStepChange={updateStep} />
<StyledForm form={form} onFinish={onFinish}>
<컴포넌트1 />
<div className={step === 1 ? "" : "hidden"}>
<컴포넌트2 />
<div className={step === 2 ? "" : "hidden"}>
<컴포넌트3 />
</div>
<div className={step === 3 ? "" : "hidden"}>
<컴포넌트4 />
</div>
<div className={step === 4 ? "" : "hidden"}>
<컴포넌트5 />
</div>
<BottomStepNavigator
prev={handlePrevButton}
next={handleNextButton}
/>
</StyledForm>
</>
);
}
export default Page;
const StyledForm = styled(Form)`
.hidden {
display: none;
}
`;
바꾼 구현 방법에서는 display: none 속성을 사용했습니다.
두 방법의 차이점을 소개드릴게요!
조건부 렌더링을 사용하여 컴포넌트 숨기기
조건부 렌더링을 사용하면 특정 조건에 따라 컴포넌트를 렌더링할지 말지를 결정합니다.
이 방식에서는 조건이 false일 경우, 해당 컴포넌트는 DOM에 존재하지 않게 됩니다.
불필요한 컴포넌트를 렌더링하지 않으니 잘 구현한 것이 아닐까 생각을 했어요 머쓱 🤦🏻♀️
하지만, 컴포넌트가 DOM에 존재하지 않으므로, 폼 필드의 값이 유지되지 않는 문제가 발생했습니다.
페이지 이동 시 기존의 상태를 유지하기 어려웠던 거죠..!
display: none 속성으로 컴포넌트 숨기기
이 방식에서는 모든 단계의 컴포넌트를 DOM에 존재하게 두고 화면에서만 숨깁니다.
이는 폼 필드의 값이 유지되므로, 페이지 이동 시에도 입력된 값을 쉽게 접근할 수 있고
또 상태관리가 용이하여 최종 제출 시 모든 값을 쉽게 접근할 수 있어요.
결국, DOM에 요소가 존재하느냐 아니냐가 중요한 이슈라는 것을 깨닫게 되었어요.
좋은 학습 경험이 되었답니다! 굿!
글 쓰는 속도가 개발 속도보다 현저히 느려서 ..
여러 고민을 잘 전달할 수 있을지 모르겠지만,
점점 방법을 찾아가겠죠?! 일단 첫 번째 슬기로운 회사생활 끝~
'슬기로운 회사생활' 카테고리의 다른 글
서명 및 도장 SVG로 만들기 | 전자직인 생성 라이브러리 | 간편 온라인 도장 | sign-generator (0) | 2024.08.08 |
---|---|
로컬 개발 환경에서 dev 환경으로 리다이렉트 되는 현상, 환경 변수 설정 실수 피하기 (0) | 2024.07.16 |
2차 개발이 시작되고 피그마 디자인이 ... 공통 컴포넌트 제작 어떻게 하지? (0) | 2024.07.16 |
안녕하세요, 초원입니다. 👩🏻💻
두 번의 외주 프로젝트를 맡아 개발을 진행해보니, 이제야 작업 흐름이 눈에 들어오기 시작했는데요!
공통 컴포넌트 제작, API 연동 등 .. 중요한 작업이 많지만,
그 중에서도 폼 관리, 상태 관리는 굉장히 까다로운 작업이라고 느끼는 요즘입니다 ..
하다보면 익숙해져서 뭐가 어려웠었는지 다 잊을 것 같아 이제부턴 기록을 잘 남겨보려고 해요. 예 👏🏻 👏🏻 👏🏻
그럼 시작해볼까요 ~
프론트엔드 개발의 세 가지 패턴: 설문조사 패턴
글의 제목은 토스 SLASH23 퍼널: 쏟아지는 페이지 한 방에 관리하기의 한 문장을 인용해 보았어요.
해당 세션에서는 프론트엔드 개발의 대표적인 세 가지 패턴을 소개합니다.
1. 목록 페이지에서 클릭 시 상세 페이지로 이동하는 상점 패턴
2. 하나의 페이지로 관리되는 단일 페이지 앱
3. 단계 별로 페이지 이동 후 최종 페이지에 도달하는 설문조사 패턴
이 중 제가 개발하고 있는 서비스는 설문조사 패턴을 사용하여
단계 별로 폼(form)을 관리하고 최종 페이지에서 제출하는 형태로 진행이 됩니다.
설문조사 패턴



흐름은 다음과 같습니다.
- (왼편) 진행 단계를 확인할 수 있는 ProgressIndicator의 각 단계를 누르면 해당 단계로 이동할 수 있다.
- (하단) BottomStepNavigator에 있는 이전, 다음 버튼을 누르면 한 단계씩 전,후로 이동할 수 있다.
- 마지막 단계에 도달 후 버튼을 누르면 모든 단계에서 작성한 폼이 한 번에 제출된다.
구성은 다음과 같습니다.
- 1단계: 폼 없음
- 2단계: 폼 없음
- 3단계: 폼 3개
- 4단계: 폼 2개 + (조건부 렌더링으로 생성된 추가 폼)
- 5단계: 폼 없음
실제 요구사항은 좀 더 복잡하지만, 이번 글에서는 이 정도만 가져오면 될 것 같네요!
Ant Design Form
아 참, 사용 중인 라이브러리를 소개해 드릴게요.
여러 분은 폼을 어떻게 관리하시나요?
useState와 onChange로 하나하나 관리해줄 수도 있지만,
요즘은 잘 되어 있는 라이브러리가 너무도 많죠 🙆🏻♀️
저번 프로젝트에서는 zod + react-hook-form 조합으로 폼을 관리했는데,
이번엔 Ant Design Form을 사용했습니다. (팀에서 UI 라이브러리로 사용 중이기 때문에~?)
Ant Design Form 컴포넌트 사용하여 폼 관리하기
import { Form, Input } from "antd";
function Parent() {
const [form] = Form.useForm();
return (
<Form form={form}>
<Child1 />
<Child2 />
</Form>
);
}
function Child1() {
return (
<Form.Item name="field1" label="Field 1">
<Input />
</Form.Item>
);
}
function Child2() {
return (
<Form.Item name="field2" label="Field 2">
<Input />
</Form.Item>
);
}
export default Parent;
어떻게 적용할 수 있는지 간단한 예제 코드를 가져와봤어요.
1. 폼 인스턴스 생성
const [form] = Form.useForm();
useForm 훅을 사용하여 폼 인스턴스를 생성하고 이를 Form 컴포넌트에 전달함으로써 폼 상태를 관리할 수 있습니다.
2. Form 컴포넌트
<Form form={form}>
<Child1 />
<Child2 />
</Form>
Form 컴포넌트는 form 인스턴스를 통해 폼 필드의 상태와 유효성을 관리합니다.
이를 통해 전체 폼의 상태를 일관되게 유지할 수 있습니다.
3. 폼 필드 정의:
<Form.Item name="field1" label="Field 1">
<Input />
</Form.Item>
각 필드는 Form.Item으로 정의되며, name 속성으로 폼 필드의 이름을 지정하고 label 속성으로 라벨을 설정합니다.
이 구조를 통해 폼 데이터를 쉽게 관리하고, 각 필드의 상태를 추적할 수 있습니다.
위와 같이 폼 관리를 간편하게 할 수 있으며, 폼 상태와 유효성 검사를 쉽게 처리할 수 있어요!
그래서 구현을 어떻게 했나요?
function Page() {
const [form] = Form.useForm();
const { step, updateStep, formValues } = useStore();
useEffect(() => {
form.setFieldsValue({ ...formValues });
}, []);
const handlePrevButton = () => {
// 이전 단계로 이동
};
const handleNextButton = () => {
// 다음 단계로 이동
};
const onFinish = (values: ValuesType) => {
console.log("Form values: ", values);
};
return (
<>
<ProgressIndicator selectedStep={step} onStepChange={updateStep} />
<StyledForm form={form} onFinish={onFinish}>
<컴포넌트1 />
{step === 1 && <컴포넌트2 />}
{step === 2 && <컴포넌트3 />}
{step === 3 && <컴포넌트4 />}
{step === 4 && <컴포넌트5 />}
<BottomStepNavigator
prev={handlePrevButton}
next={handleNextButton}
/>
</StyledForm>
</>
);
}
export default Page;
처음에는 step의 상태에 따라 조건부 렌더링을 하도록 컴포넌트를 구현하였습니다.
ProgressIndicator 조작에 따라 단계가 잘 이동되고 해당 페이지도 잘 보여졌지만,
문제는 마지막에 제출할 때 formValues를 콘솔로 찍어보니 아무 것도 안담기지 말입니다 ..?
처음엔 form 인스턴스가 props로 잘 전달되지 않아서 발생한 문제인 줄 알고,
폼을 가지고 있는 3, 4 단계에서 form values를 출력해보니 모두 잘 나오는 것이 확인됐습니다.
마지막 페이지에서 확인할 수 없던 것이라면 .. Antd form이 어떤 이유에서 name 값을 인지하지 못하나?
의심을 하였고 아래와 같이 구현 방식을 바꿔보았습니다.
이렇게 바꿨어요
function Page() {
const [form] = Form.useForm();
const { step, formValues } = useStore();
useEffect(() => {
form.setFieldsValue({ ...formValues });
}, []);
const handlePrevButton = () => {
// 이전 단계로 이동
};
const handleNextButton = () => {
// 다음 단계로 이동
};
const onFinish = (values: ValuesType) => {
console.log("Form values: ", values);
};
return (
<>
<ProgressIndicator selectedStep={step} onStepChange={updateStep} />
<StyledForm form={form} onFinish={onFinish}>
<컴포넌트1 />
<div className={step === 1 ? "" : "hidden"}>
<컴포넌트2 />
<div className={step === 2 ? "" : "hidden"}>
<컴포넌트3 />
</div>
<div className={step === 3 ? "" : "hidden"}>
<컴포넌트4 />
</div>
<div className={step === 4 ? "" : "hidden"}>
<컴포넌트5 />
</div>
<BottomStepNavigator
prev={handlePrevButton}
next={handleNextButton}
/>
</StyledForm>
</>
);
}
export default Page;
const StyledForm = styled(Form)`
.hidden {
display: none;
}
`;
바꾼 구현 방법에서는 display: none 속성을 사용했습니다.
두 방법의 차이점을 소개드릴게요!
조건부 렌더링을 사용하여 컴포넌트 숨기기
조건부 렌더링을 사용하면 특정 조건에 따라 컴포넌트를 렌더링할지 말지를 결정합니다.
이 방식에서는 조건이 false일 경우, 해당 컴포넌트는 DOM에 존재하지 않게 됩니다.
불필요한 컴포넌트를 렌더링하지 않으니 잘 구현한 것이 아닐까 생각을 했어요 머쓱 🤦🏻♀️
하지만, 컴포넌트가 DOM에 존재하지 않으므로, 폼 필드의 값이 유지되지 않는 문제가 발생했습니다.
페이지 이동 시 기존의 상태를 유지하기 어려웠던 거죠..!
display: none 속성으로 컴포넌트 숨기기
이 방식에서는 모든 단계의 컴포넌트를 DOM에 존재하게 두고 화면에서만 숨깁니다.
이는 폼 필드의 값이 유지되므로, 페이지 이동 시에도 입력된 값을 쉽게 접근할 수 있고
또 상태관리가 용이하여 최종 제출 시 모든 값을 쉽게 접근할 수 있어요.
결국, DOM에 요소가 존재하느냐 아니냐가 중요한 이슈라는 것을 깨닫게 되었어요.
좋은 학습 경험이 되었답니다! 굿!
글 쓰는 속도가 개발 속도보다 현저히 느려서 ..
여러 고민을 잘 전달할 수 있을지 모르겠지만,
점점 방법을 찾아가겠죠?! 일단 첫 번째 슬기로운 회사생활 끝~
'슬기로운 회사생활' 카테고리의 다른 글
서명 및 도장 SVG로 만들기 | 전자직인 생성 라이브러리 | 간편 온라인 도장 | sign-generator (0) | 2024.08.08 |
---|---|
로컬 개발 환경에서 dev 환경으로 리다이렉트 되는 현상, 환경 변수 설정 실수 피하기 (0) | 2024.07.16 |
2차 개발이 시작되고 피그마 디자인이 ... 공통 컴포넌트 제작 어떻게 하지? (0) | 2024.07.16 |