슬기로운 회사생활

쏟아지는 페이지들, 프론트엔드에서 어떻게 설계하면 좋을까?

김필드 2024. 7. 9. 21:54

안녕하세요, 초원입니다. 👩🏻‍💻
 
두 번의 외주 프로젝트를 맡아 개발을 진행해보니, 이제야 작업 흐름이 눈에 들어오기 시작했는데요!
공통 컴포넌트 제작, API 연동 등 .. 중요한 작업이 많지만,
그 중에서도 폼 관리, 상태 관리는 굉장히 까다로운 작업이라고 느끼는 요즘입니다 ..
 
하다보면 익숙해져서 뭐가 어려웠었는지 다 잊을 것 같아 이제부턴 기록을 잘 남겨보려고 해요. 예 👏🏻 👏🏻 👏🏻
그럼 시작해볼까요 ~
 
 

프론트엔드 개발의 세 가지 패턴: 설문조사 패턴

글의 제목은 토스 SLASH23 퍼널: 쏟아지는 페이지 한 방에 관리하기의 한 문장을 인용해 보았어요.
 
해당 세션에서는 프론트엔드 개발의 대표적인 세 가지 패턴을 소개합니다.
1. 목록 페이지에서 클릭 시 상세 페이지로 이동하는 상점 패턴
2. 하나의 페이지로 관리되는 단일 페이지 앱
3. 단계 별로 페이지 이동 후 최종 페이지에 도달하는 설문조사 패턴
 
이 중 제가 개발하고 있는 서비스는 설문조사 패턴을 사용하여
단계 별로 폼(form)을 관리하고 최종 페이지에서 제출하는 형태로 진행이 됩니다.
 
 

설문조사 패턴

현재 개발중인 UI 스켈레톤

 
흐름은 다음과 같습니다.
  - (왼편) 진행 단계를 확인할 수 있는 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에 요소가 존재하느냐 아니냐가 중요한 이슈라는 것을 깨닫게 되었어요.

좋은 학습 경험이 되었답니다! 굿!

 

글 쓰는 속도가 개발 속도보다 현저히 느려서 ..

여러 고민을 잘 전달할 수 있을지 모르겠지만,

점점 방법을 찾아가겠죠?! 일단 첫 번째 슬기로운 회사생활 끝~