회사에서 Form을 만들어서 문의를 받을 일이 점차 많아질 것으로 예상했다.
때문에 셀렉트박스나 체크박스 등 다양한 인풋과 인풋에 해당하는 핸들러를 매번 일일이 만들어주기보다는
인풋도 유형별로 만들어두고, 각 유형에 따른 핸들러도 함수 하나로 작성해두고 싶었다!
예를 들어, 기존에 "회사 규모"와 "업종"만 셀렉트 박스로 받고 있다가, 추후에 "예상 교육 인원" 값도 받게 됐다고 했을 때,
필드 하나를 추가해주기 위해, 핸들러 함수를 따로 작성하는 것은 비효율적이라고 생각했기 때문이다.
문제는, 하나의 핸들러함수이지만,
각각의 핸들러는 자신이 할당된 셀렉트 박스가 컨트롤하는 필드가 "회사 규모"와 "업종", 그리고 "예상 교육 인원" 중 어떤 필드인지를 알아차릴 방법이 필요했다.
그리하여, 내가 처음에 구현했던 방식은 아래와 같았다.
interface SelectBoxProps {
// 생략
onChange?: (e: MouseEvent<HTMLButtonElement>) => void;
}
const SelectBox = ({ id, onChange,
//...이하생략
}: SelectBoxProps) => {
// 생략
return (
<Wrapper>
<button />
<OptionList>
{options.map(({ value, name }, index) => (
<li>
<button
className={id} // 해당 셀렉트박스가 어떤 필드의 응답을 받는지 className에 할당해둠 (조금 어설픈 방법이라 생각)
value={value} // 옵션의 밸류도 미리 버튼에 저장해둠
onClick={e => {
if (onChange) {
onChange(e); // 위에 기록해둔 정보가 담긴 이벤트 객체를 통째로 핸들러에 넘겨줌
}
}}
>
{name}
</button>
</li>
))}
</OptionList>
</Wrapper>
);
};
const handleOptionSelect = (e: ChangeEvent<HTMLButtonElement>) => {
const target = e.target;
const id = target.getAttribute('class');
const newValue = target.getAttribute('value');
if (!id) return;
const newFormAnswer = cloneDeep(formAnswer);
newFormAnswer[id] = { type: newValue, direct: formAnswer[id].direct };
setFormAnswer(newFormAnswer);
};
그런데 이번에 해당 프로젝트에도 디자인 시스템을 도입하면서, 주요한 컴포넌트들을 손 보게 되었다!
셀렉트박스를 맡았던 동료분께서, 내가 기존에 작성해둔 SelectBox를 리팩토링 해주시면서,
onChange 함수가 이벤트 객체 전체가 아니라, value만 받으면 안 될지 먼저 제안을 주셨다.
(결국 이벤트 객체를 통해서 받고자 하는 정보는 e.target.value
일텐데 매번 value값을 꺼내서 쓰는 게 비효율적이라고 생각하셨던 것 같다.)
동료의 요청을 간단하게 요약하면 위와 같았다!
나의 주요한 고민은 아래와 같았다!
그렇다면 대체 어떻게 핸들러함수에 문의 양식 중 어떤 필드를 컨트롤하는지에 대한 플래그를 남겨줄 수 있을까!
그렇다고 핸들러의 둘째 인자로 id 옵셔널로 하여 넘겨줄 수도 없는 노릇이었다!
대강 아래와 같은 형태!
interface SelectBoxProps {
// 생략
onChange?: (e: MouseEvent<HTMLButtonElement>, id?: string) => void;
}
const SelectBox = ({ id, onChange,
//...이하생략
}: SelectBoxProps) => {
// 생략
return (
<Wrapper>
<button />
<OptionList>
{options.map(({ value, name }, index) => (
<li>
<button
onClick={e => {
if (onChange) {
onChange(e, id); // 그냥 일반적으로 SelectBox를 사용한다고 할 때, 굉장히 어색한 형태
}
}}
>
{name}
</button>
</li>
))}
</OptionList>
</Wrapper>
);
};
함수의 외부에 존재하지만, 특정 값을 함수가 기억해둘 수 있게 만드는 방법이 무엇이 있을까!?
그런 고민을 하다가 클로저를 떠올렸다.
기존에 작성해두었던 handleOptionSelect
를 반환하는 makeSelectBoxHandlerBindId
를 작성했다.
그리고 새로 작성한 함수가 selectBoxId
를 받도록 하였다.
const makeSelectBoxHandlerBindId = (selectBoxId: string) => {
return (value: string) => {
const newFormAnswer = cloneDeep(formAnswer);
newFormAnswer[selectBoxId] = { type: value, direct: formAnswer[selectBoxId].direct };
setFormAnswer(newFormAnswer);
};
};
이렇게 되면 반환된 handleOptionSelect
함수는 따로 id를 인자로 받지 않지만, 외부함수의 실행이 종료되어도, 계속해서 selectBoxId
를 참조할 수 있게 된다!
그래서 아래와 같이 SelectBox에 (value: string) => void 타입의 onChange를 전달할 수 있게 되었다!
<SelectBox
onChange={value => {
const selectBoxHandler = questionChangeHandlers[questionType.SELECT_BOX].defaultHandler(id);
if (!selectBoxHandler) return;
selectBoxHandler(value);
}}
/>
클로저를 실제 사례에 적용한 건, 이번이 처음인 것 같아서, 조금 창피하기는 하면서도 재밌는 기억이라 포스팅을 남겨보았다!
와 정호님 이런거 왜 공유안해주셨어요?? 클로저실제사례라니 너무 떨려요
선댓후읽 합니다