이번엔 리팩토링을 진행해보자! 리팩토링 대상은 다음과 같다.
<Form.Control id="email" type="email" value={email} onChange={handleChange} required />```
현재는 Form.Control 자체에 값을 넣어주어서 처리하고 있다. 이게 나쁘다는 건 아니지만, 현재 상황에서는 두 가지의 이유에서 바꿀 이유가 있다고 판단했다.
<Form.Control asChild> <input type="password" id="password" value={password} onChange={onChange} required /> </Form.Control>```
이러한 이유로 Radix UI에서는 asChild라는 Prop이 존재한다. 이 속성을 쓰게되면 이벤트들이 아래 자식에게 위임된다.
import { useState, ReactElement } from "react"; type StepProp = { title: string; element: ReactElement; }; const useMultiStepForm = (steps: StepProp[]) => { const [currentStepIndex, setCurrentStepIndex] = useState(0); const prev = () => { setCurrentStepIndex((index) => (index <= 0 ? 0 : index - 1)); }; const next = () => { setCurrentStepIndex((index) => index >= steps.length - 1 ? index : index + 1, ); }; ? return { currentStepIndex, currentTitle: steps[currentStepIndex].title, currentStep: steps[currentStepIndex].element, isFirstStep: currentStepIndex === 0, isLastStep: currentStepIndex === steps.length - 1, prev, next, }; }; export default useMultiStepForm;```
현재 useMultiStepForm Hook이다. 지금도 괜찮은 것 같지만~ 좀 더 구체화하고 추가하고 싶었다.
import { ComponentType, useState } from "react"; export type StepProp = { title: string; Component: ComponentType\<any>; }; export type MultiStepFormType = { currentStepIndex: number; currentTitle: string; isFirstStep: boolean; isLastStep: boolean; progressDirection: number; CurrentForm: ComponentType\<any>; prevStep: () => void; nextStep: () => void; formData: object; updateFields: (field: object) => void; }; const useMultiStepForm = ( steps: StepProp[], initialFormData: object, ): MultiStepFormType => { const [currentStepIndex, setCurrentStepIndex] = useState(0); const [formData, setFormData] = useState(initialFormData); const [progressDirection, setProgressDirection] = useState(1); const updateFields = (field: object) => { setFormData((prev) => ({ ...prev, ...field })); }; const prevStep = () => { setCurrentStepIndex((index) => (index <= 0 ? 0 : index - 1)); setProgressDirection(-1); }; const nextStep = () => { setCurrentStepIndex((index) => index >= steps.length - 1 ? index : index + 1, ); setProgressDirection(1); }; return { currentStepIndex, currentTitle: steps[currentStepIndex].title, CurrentForm: steps[currentStepIndex].Component, isFirstStep: currentStepIndex === 0, isLastStep: currentStepIndex === steps.length - 1, progressDirection, prevStep, nextStep, formData, updateFields, }; }; export default useMultiStepForm;```
하나하나 보자.
StepProp의 프로퍼티 키 변경
export type StepProp = { title: string; Component: ComponentType\<any>; };```
요녀석은 element에서 Component로 변경되었다. 처음 의도는 <Component />를 넣는 녀석이라 element라는 이름이 맞았지만, 지금은 Component 자체를 넣는 녀석이 되어서 그에 맞게 변경하였다. any를 쓰고 싶진 않았지만,,, 그렇게 됐다(ㅋㅋ)
추가된 상태
const [currentStepIndex, setCurrentStepIndex] = useState(0); const [formData, setFormData] = useState(initialFormData); const [progressDirection, setProgressDirection] = useState(1);```
원래는 currentStepIndex라는 녀석만 있었는데, formData와 progressDirection이 추가되었다. formData까지는 충분히 넣을 수 있는 녀석인데, progressDirection은 분명히 이 프로젝트에서만 필요한 존재일 것.
아무튼 생각해보니, formData역시 Hook에서 관리를 충분히 해줄 수 있을 것 같아서 추가해주었다.
추가된 상태에 따른 코드
const updateFields = (field: object) => { setFormData((prev) => ({ ...prev, ...field })); }; const prevForm = () => { // ... setProgressDirection(-1); }; const nextForm = () => { // ... setProgressDirection(1); };
추가된 상태들에 대한 코드들. formData를 위해서 field를 받아 업데이트해주는 녀석과, 이전과 다음 버튼에 따라 진행방향을 바꿔주는 코드를 추가했다.
formData나 field type은 Record 타입으로 선언해줄 수 있을 것 같은데, 일단 내버려뒀다(...)
prev, next가 아닌 prevStep, nextStep
const prevStep = () => { }; const nextStep = () => { };
그리고 prev와 next의 이름도 명확하게 바꿨다. 대상이 확실해짐!
한두 개가 바뀐 게 아니라 거의 구조가 다 바뀐 느낌인데, 가장 포인트가 되는 부분은 입력을 받는 부분이 되겠다.
const updateFields = (fields: Partial\<FormDataProps>) => { setFormData((prev) => ({ ...prev, ...fields })); }; const { currentTitle, currentStep, prev, next, isFirstStep, isLastStep } = useMultiStepForm([ { title: "Input Email", element: <EmailForm {...formData} updateFields={updateFields} />, }, { title: "Input Password", element: <PasswordForm {...formData} updateFields={updateFields} />, }, { title: "Input Nickname", element: <NicknameForm {...formData} updateFields={updateFields} />, }, ]);```
처음엔 이렇게 element 자체를 넣어서 줬는데 이로 인해서 생기는 단점이 있었다.
const multiStepList: StepProp[] = [ { title: "Input Email", Component: EmailForm, }, { title: "Input Password", Component: PasswordForm, }, { title: "Input Nickname", Component: NicknameForm, }, ]; const initialData: FormDataProps = { email: "", password: "", passwordConfirm: "", nickname: "", }; const { CurrentForm } = useMultiStepForm(multiStepList, initialData); const handleChange = (event: ChangeEvent\<HTMLInputElement>) => { updateFields({ [event.target.id]: event.target.value }); }; () => <CurrentForm {...formData} onChange={handleChange} />``
지금 관련된 코드들이 다 흩어져있어서 그렇지만 그 코드들만 모아보면 대충 이렇게 된다. Component 자체를 받으니 나중에 Prop을 넘겨주는 것도 한 줄의 코드로도 할 수 있게 되었다.
여담으로 처음에는 Javascript는 무슨 변수에 함수가 들어갈 수 있냐고 되게 투덜댄 기억이 있는데,,, 이젠 이 느슨함 못 잃어...(ㅋㅋ)
이 부분에 대해서는 나도 계속해서 찾아보고 배워가는 중이라 어떤 게 효율적이고 어떤 게 메리트가 있는 건지 잘 모르겠지만, 최근 내가 마음에 들어하는 방법으로 처리해봤다.
[NAME].styled.ts, [NAME].types.ts가 된다.export const Page = styled("div", { }); export const Container = styled("div", { });```
import * as S from './[NAME].styled.tx 로 불러와서 다음과 같이 쓰는 것.<S.Page> <S.Container> ... </S.Container> </S.Page>```
요새는 이렇게 쓰는 게 굉장히 명확하다는 생각이 들었다. Style Component인지, 아니면 React Component인지 구분이 딱 되니까! 아참, 그리고 참고로 Type은 별칭을 안 쓰는 편이 나은 것 같기도.. 굳이 다른 것과 구분해줄 필요가 없으니 말이다. (아직은 필요성을 못 느꼈다)
여튼 이런 식으로 다 구분해주었다!
여러 페이지에서 필요한 컴포넌트가 아니라 한 페이지 안에서 구조상 나눠주어야 할 경우가 있는데, 이럴 때도 위와 같이 관리해주기로 했다.
파일은 [NAME].component.tsx 로 만들고, 별칭을 붙여 관리해주었다.
코드가 너무 더러워서,,, 패스한다.
<S.Page> <S.Container> <S.Header> <button/> <C.AnimateTitle /> <button /> </S.Header> <S.Body> <C.AnimateForm /> </S.Body> </S.Container> </S.Page>```
Prop은 다 떼고 구조만 가져왔다. 훠~얼씬 간결해지고 보기가 좋아졌다. 이거지...
전체적으로 어렵지 않은 리팩토링이었지만 나르음 고민이 많았던 리팩토링. 리팩토링의 제 맛은 역시 Before과 After를 비교하는 재미랄까.... 궁금한 분들은 아래의 링크에서 비교해보시길(ㅋㅋ)