들어가며
저번 추상화강의에 이어서 비즈니스 로직과, Compound Componenet, 그리고 캡슐화에 대한 내용까지 들었습니다. 저도 프로젝트를 진행하면서 재사용성을 올릴 수 있는 방법에 대해 고민했었고, 비즈니스 로직까지 구분했던 경험이 있었습니다. 이런 경험을 바탕으로 내 프로젝트에 어떻게 적용할 수 있을까를 고민하며, 강의를 들었더니 '이거 조금 분리할 수 있겠는데?'라는 아이디어를 얻었고, 직접 리팩토링까지 해보게 되었습니다.
우선 저의 목표는 아래 컴포넌트의 비즈니스 로직을 분리하는 것입니다. 아래 컴포넌트는 이모지를 클릭시 바로 서버에 데이터를 전송하고, 이용자에게 평가가 반영되었다는 토스트 메시지를 이용자에게 보여주는 컴포넌트입니다.
// FeedbackSelet.jsx
export default function FeedbackSelect({ UI타입, 질문번호 }) {
const { nowSelected, etNowSelectedFeedback, isToast } = useEmotionFeedback(질문번호);
return (
<StyledFeedbackSelect>
{feedbackOptions.map(([option, label, score], idx) => {
return (
<FeedbackEmotion
emotion={option}
key={['Emotion', idx]}
isActivated={nowSelected === score}
onClick={() => setNowSelectedFeedback(score)}
>
<FeedbackEmotion.Label label={label} />
{isToast && nowSelected === score && (
<Toast style={{ width: 130 }}>
<Text label="평가가 반영되었어요!" color={Theme.color.white} size="12px" bold="400" />
</Toast>
)}
</FeedbackEmotion>
);
})}
</StyledFeedbackSelect>
이 컴포넌트는 고객 Feedback을 이용자에게 제공한다는 비즈니스 로직을 가지고 있습니다. 즉, Select라는 컴포넌트가 있다면, Feebdack 제공이라는 비즈니스 로직과 합쳐 위와 같은 동일한 기능을 하는 컴포넌트를 만들 수 있을 것 같습니다. 그리고, 마침 저는 비즈니스 로직이 제거된 Select컴포넌트를 가지고 있습니다.
Select 조금 더 추상화해보자
우선 저의 Select 컴포넌트를 소개하겠습니다. Trigger를 클릭 시 드롭박스가 나오게 되고, 드롭박스에 아이템을 클릭하면, 클릭한 값이 선택됩니다.
function Select({ trigger, options, setValue, ...rest }) {
const [isModal, setIsModal] = useState(false);
const changeValue = (e, { id, value }) => {
e.stopPropagation();
setValue(id, value);
setIsModal(false);
};
return (
<StyledSelect {...rest}>
{cloneElement(trigger, {
onClick: (e) => {
e.stopPropagation();
setIsModal((prev) => !prev);
},
})}
{isModal && (
<div className="selects">
{options?.map((data) => {
return (
<div
key={['category', data.id]}
role="presentation"
onClick={(e) => changeValue(e, data)}
className="option"
value={data.value}
>
<Text label={data.value} size="16px" />
</div>
);
})}
</div>
)}
</StyledSelect>
);
}
Select를 사용한 컴포넌트는 아래에 쿠팡 아이템을 렌더링 하는 컴포넌트입니다. 아래와 같이 드롭다운에서 값이 선택되면 카드컴포넌트들을 바꾸고 있습니다.
function CoupangRecommend () {
return (
...
{!categoryLoading && (
<Select
options={categories.map((data) => data)}
setValue={changeNowCategory}
style={{
position: 'absolute',
top: 16,
right: 14,
}}
trigger={
<SelectInput
value={nowCategory.value}
bg={type === 'sara' ? Theme.color.saraPrimary : Theme.color.maraPrimary}
type={type}
/>
}
/>
)}
</Title>
<Cards datas={showingData} />
...
)
}
그러면, 이 Select에 Feedback이라는 비즈니스로직을 합친다면 FeedbackComponent를 만들 수 있을까요?
아쉽게도, 이 상태로를 그렇게 할 수 없을 것 같습니다. 물론, 비즈니스 로직 자체는 분리되어 있지만, Select 컴포넌트는 드롭박스 UI에 의존하고 있기 때문입니다. 그래서 이 컴포넌트를 재사용하기 위해서는 조금 더 추상화가 필요할 것 같습니다.
현재의 Select는 Select의 후보군을 드롭다운으로 렌더링 시키는 역할과 value변화에 따르는 액션 두 가지의 역할을 수행하고 있습니다. 이러한 Select 컴포넌트에게 후보군의 렌더링을 어떻게 할지 고민하게 하는 역할은 제거하고, 값이 변경되었을 때에 함수실행을 담당하는 역할만을 준다면 재사용성을 더 증가시킬 수 있을 것 같습니다.
function Select({ onChange, children, value, ...rest }) {
return (
<StyledSelect {...rest}>
<input type="hidden" value={value} onChange={onChange} />
{children}
</StyledSelect>
);
}
function List({ children, cssStyle }) {
return <StyledList cssStyle={cssStyle}>{children}</StyledList>;
}
Select 컴포넌트가 현재의 데이터와 데이터가 변경됐을 때의 동작(onChange)만을 다루게 하기 위해 Compound Components 활용했습니다. Select List의 뷰는 Select.List를 통해서 부모컴포넌트에서 상속받기 때문에 Select 컴포넌트는 이벤트를 통해서 값을 변경했을 때, 실행하는 로직처리에만 집중할 수 있게 됐습니다.
비즈니스 로직과 컴포넌트 합치기
Select 컴포넌트를 리팩터링 하면서 조금 더 순수한 형태의 컴포넌트로 바꿀 수 있었습니다. 이제 이러한 컴포넌트를 Feedback을 제출하는 비즈니스 로직과 합쳐보겠습니다.
합치는 과정은 전혀 어려울 게 없었습니다. 기존에 비즈니스로직을 담당하는 훅을 미리 개발해 놨기 때문에, FeedbackSelect에서는 훅과 만들어진 Select Component를 합치기만 하면 됩니다. Select에 value가 변경할 때마다, 훅에서 만들어 놓은 함수를 onChange에 등록하여 작동만 하면 됩니다.
function FeedbackSelect () {
const { nowSelected, setNowSelectedFeedback, isToast } = useEmotionFeedback(quesionId);
return (
<StyledFeedbackSelect>
<Select value={nowSelected}>
<Select.List cssStyle={feedbackStyle}>
{feedbackOptions.map(([option, label, score], idx) => {
return (
<FeedbackEmotion
type={type}
emotion={option}
key={['Emotion', idx]}
isActivated={nowSelected === score}
onClick={() => setNowSelectedFeedback(score)}
>
<FeedbackEmotion.Label label={label} />
{isToast && nowSelected === score && (
<Toast style={{ width: 130 }}>
<Text label="평가가 반영되었어요!" color={Theme.color.white} size="12px" bold="400" />
</Toast>
)}
</FeedbackEmotion>
);
})}
</Select.List>
</Select>
</StyledFeedbackSelect>)
}
쿠팡 컴포넌트 역시 동일하게 개발이 가능합니다. Select에서 값이 변경될 때, 새로운 쿠팡 API로직을 불러오는 함수를 등록했습니다. '달라지는 것은 어떻게 SelectList를 보여줄 것이고, 어떻게 값을 바꿀 것인지'입니다. 하지만 그 부분은 Select에서 직접 구분하는 것이 아닌 비즈니스 로직을 담당하는 컴포넌트에서 어떻게 보이게 할지 정하게 하며 Select는 자신의 기능에만 집중하도록 했습니다.
export default function CounpangRecommend({ type }) {
return (<StyledCounpoangRecommend>
...
{카테고리 목록 FETCH완료 && (
<Select value={nowCategory} onChange={changeNowCategory}>
<Select.List >
<Dropdown
trigger={
<DropInput />
}
>
{categories.map((data) => {
return (
<Dropdown.Item ... />
);
})}
</Dropdown>
</Selet.List>
</Select>
)}
</Title>
<Cards datas={showingData} />
{!itemLoading && !categoryLoading && (
<Pagination leftClick={setPrevPage} nowPage={nowPage} maxPage={maxPage} rightClick={setNextPage} type={type} />
)}
<div className="bottom">
<Text
color={Theme.color.darkGray}
label="*위 컨텐츠는 쿠팡 파트너스 활동의 일원으로 금전적 대가를 취할 수 있습니다"
style={{
color: `rgba(0,0,0,0.1)`,
fontSize: 10,
}}
/>
</div>
</StyledCounpoangRecommend>)
마치며
동일한 기능(여러 개의 후보군 중 하나를 선택)을 가지는 두 컴포넌트가 다른 UI를 가진다는 이유로, 컴포넌트를 재사용하지 못하고 새로 코드를 작성해야 한다는 점에서 조금 더 나은 방법이 없는가에 대해 고민했습니다.
이때, 추상화를 통해서 내가 가진 컴포넌트의 기능을 조금 더 간단하게 표현했고, Compound Componenet Pattern을 활용해서 컴포넌트가 UI변화에 좀 더 유연하게 대응할 수 있도록 했습니다.
완성된 코드를 리팩터링 하면서 코드가 더 복잡해지고 혹시, 새로운 문제를 발생시킬까 봐 걱정도 했지만, 다행히 제가 생각한 대로 코드를 나누고 재사용성까지 증가시키면서 짜릿한 감정을 느낄 수 있었습니다. 이번 리팩토링이 큰 이슈없이 잘 성공한 이유는, 이번 리팩토링과정에서 추상화를 잘했기 때문이라기보다, 프로젝트를 하면서 디자인 패턴부터 컴포넌트 재사용성, 비즈니스 로직 분석 및 캡슐화를 위해 고민해 왔기 때문이라고 생각합니다.
마치 과거의 저의 노력들이 현재의 저를 도와주는 느낌이었습니다. 아마 컴포넌트 설계와 클린코드라는 것이 결국 미래의 저와 동료들에게 도움을 주기 위한 지금의 노력이라고 생각해 봅니다.
'개발' 카테고리의 다른 글
[UI] 아코디언 컴포넌트 (feat: UI 요소 직접 만들기) (0) | 2025.01.06 |
---|---|
[가상스크롤] 가상스크롤 직접 개발하기 (2) | 2024.10.19 |
코드 리팩토링 (액션, 데이터, 계산 나누기) (0) | 2023.12.08 |
CORS (0) | 2023.12.06 |
[SaraMara] 아토믹 디자인 도입하기 (2) (0) | 2023.11.29 |