기존에 진행했던 SNS 플랫폼 기반 익명/기명 편지 서비스 "대박 사건" 프로젝트에서 각 페이지에 따로 구현되어 존재했던 편지 작성, 이전에 작성한 편지 수정, 댓글 작성 컴포넌트(모두 Textarea로 이루어짐)를 공통 컴포넌트로 묶어 재사용성을 높여보았다.
위 구조로 완성된 UI와 문제점에 대해 간단하게 알아보자.
위 사진의 주황색으로 표시된 컴포넌트 모두가 비슷한 UI를 가지고 있고 우측의 이전 편지 수정하는 컴포넌트만 버튼 아이콘 두 개가 추가되어있다. 위 UI를 바탕으로 현재 코드의 문제점은 코드의 중복.
이를 해결하기 위해 Textarea를 하나의 고정된 UI를 가지고 있는 공통 컴포넌트로 구현하려 고민했었고, 당시 생각했던 구조는 아래와 같다.
하지만 위와 같은 설계는 치명적인 단점이 존재했었다. 아래 사진과 같이 버튼을 필요로 하는 UI에서는 위와 같은 구조를 사용할 수 없는 것이다. 만약 위와 같은 구조를 사용했다간 아래 UI와 같은 컴포넌트를 구현하기 위해 똑같은 코드를 다시 작성해야 할지도 모른다..
그래서 이런 고정된 UI가 아닌 좀 더 유연성을 확장시킬 수 있는 방법이 없을까 고민하던 중, 합성 컴포넌트 패턴에 대해 학습하여 적용하였다.
합성 컴포넌트 패턴이란 하나의 컴포넌트를 여러 가지 집합체로 분리한 뒤, 분리된 각 컴포넌트를 사용하는 쪽에서 조합해 사용하는 컴포넌트 패턴을 말합니다(카카오 기술 블로그). 이러한 합성 컴포넌트의 패턴의 장점으로는 컴포넌트를 사용할 때 개발자가 필요로 하는 서브 컴포넌트만 합성하여 사용할 수 있기 때문에 재사용성을 확장 시킬 수 있고, 하나의 컴포넌트 안에 수많은 Props를 한꺼번에 전달하지 않고 서브 컴포넌트에 분배하여 Props Drilling을 해결할 수 있다는 점입니다.
이러한 합성 컴포넌트 패턴을 적용한 구조를 한 번 보겠습니다.
위 사진처럼 Textarea 공통 컴포넌트를 사용하여 [ Post / Comment ] 페이지에서 각각 [ 이전 포스트 / 포스트 작성 ] 컴포넌트를 Textarea, Title, Content로 묶어 재사용하고 Comment 페이지에서는 Edit, Delete, Complete 버튼 컴포넌트를 추가로 합성하여 사용할 수 있습니다. 이렇게 하면 Edit, Delete, Complete이 추가된 Textarea 컴포넌트를 Comment 페이지에서 다시 작성해야 하는 불상사는 없어지겠죠? 그럼 이제 조금 더 직관적으로 코드로 확인해 보겠습니다.
/* Textarea 공통 컴포넌트의 루트 Index.tsx */
// Textarea에 포함 될 자식 컴포넌트들이 다크모드 상태를 공유 할 수 있게 Context 정의
export const TextareaContext = createContext({
darkMode: false
});
// Textarea 루트에서 사용 할 props type 정의
interface TextareaContainerProps {
children: ReactNode;
width: string;
height: string;
className?: string;
}
// Textarea의 자식 컴포넌트에서 사용 할 prop type 정의
export interface TextareaProps<T extends FieldValues> {
value?: string;
register?: UseFormRegister<T>;
registerOptions?: RegisterOptions;
placeholder?: string;
maxLength?: number;
formKey?: Path<T>;
readonly?: boolean;
width: string;
height: string;
className?: string;
}
function Textarea({
children,
width,
height,
className = ``
}: TextareaContainerProps) {
const darkMode = useAtomValue(darkAtom);
return (
<TextareaContext.Provider value={{ darkMode }}>
<Style.TextareaContainer
darkMode={darkMode}
width={width}
height={height}
className={className}>
{children}
</Style.TextareaContainer>
</TextareaContext.Provider>
);
}
// 컴포넌트는 함수로 이루어져 있고, 자바스크립트에서는 함수도 객체이므로 함수에도 속성을 추가할 수 있다.
Textarea.TextareaTitle = TextareaTitle;
Textarea.TextareaContent = TextareaContent;
Textarea.TextareaUnderLine = TextareaUnderLine;
Textarea.CompleteButton = CompleteButton;
Textarea.EditButton = EditButton;
Textarea.DeleteButton = DeleteButton;
export default Textarea;
/* Textarea의 Title 컴포넌트 */
function TextareaTitle<T extends FieldValues>({
value,
className = ``,
register,
formKey,
registerOptions,
placeholder,
maxLength,
width,
height,
readonly = false
}: TextareaProps<T>) {
const { darkMode } = useContext(TextareaContext);
return (
<Style.TextareaTitle
darkMode={darkMode}
readOnly={readonly}
defaultValue={value}
placeholder={placeholder}
maxLength={maxLength}
width={width}
height={height}
{...(register &&
formKey && {
...register(formKey, registerOptions)
})}
className={className}
/>
);
}
export default TextareaTitle;
위 코드처럼 Textarea에 Context를 정의하여 다크모드 상태를 공유하고, Textarea 컴포넌트(함수)의 속성으로 하위 컴포넌트들을 정의해 주면 준비는 끝! 추가로, 기존에 각 페이지에서 따로 존재하던 Textarea는 비제어 컴포넌트 방식을 활용한 React-Hook-Form을 사용하고 있었으므로, Textarea의 props로 register, formKey, registerOptions라는 props를 전달하여 RHF를 재사용할 수 있게 구현해 주었습니다.
그럼 이제 위 합성 컴포넌트 패턴을 사용한 공통 컴포넌트를 사용하는 쪽을 보겠습니다.
/* 편지 작성 컴포넌트 */
function Letter({ register, userName }: letterProps) {
return (
<Textarea
width={'100%'}
height={'20.3125rem'}>
<Textarea.TextareaTitle
value={
userName ? (userName === '익명' ? undefined : userName) : undefined
}
placeholder={
userName
? userName === '익명'
? '작성자명을 입력해주세요(최대 15자)'
: userName
: '작성자명을 입력해주세요(최대 15자)'
}
maxLength={15}
register={register}
formKey={'letterTitle'}
registerOptions={{
required: '작성자명은 반드시 입력해야합니다.'
}}
width={'95%'}
height={'40px'}
/>
<Textarea.TextareaUnderLine />
<Textarea.TextareaContent
placeholder={'내용을 입력하세요'}
register={register}
formKey={'letterContent'}
registerOptions={{
required: '내용을 반드시 입력해야합니다.'
}}
width={'95%'}
height={''}
/>
</Textarea>
);
}
/* 댓글 작성 컴포넌트 */
function Comment({ register, userName }: CommentProps) {
return (
<Textarea
width={'100%'}
height={'8.1875rem'}>
<Textarea.TextareaTitle
readonly={true}
value={userName}
maxLength={15}
register={register}
formKey={'commentTitle'}
registerOptions={{
required: '작성자명은 반드시 입력해야합니다.'
}}
width={'95%'}
height={'40px'}
/>
<Textarea.TextareaUnderLine />
<Textarea.TextareaContent
placeholder={'내용을 입력하세요'}
register={register}
formKey={'commentContent'}
registerOptions={{
required: '내용을 반드시 입력해야합니다.'
}}
width={'95%'}
height={''}
/>
</Textarea>
);
}
/* 이전 포스트 컴포넌트 */
function PrePost({ userName, darkMode, postId, postDetail }: PrePostProps) {
return (
<Textarea width={'100%'} height={'13.3125rem'}>
<Textarea.TextareaTitle
readonly={true}
value={postDetail && JSON.parse(postDetail.title).title}
maxLength={15}
width={'95%'}
height={'40px'}
/>
<Textarea.TextareaUnderLine />
<Textarea.TextareaContent
readonly={isEdit}
placeholder={'내용을 입력하세요'}
register={register}
formKey={'prePostContent'}
registerOptions={{
required: '내용을 반드시 입력해야합니다.'
}}
width={'95%'}
height={'3rem'}
/>
{/* 편집/완료/삭제 버튼 추가 부분 */}
{isEdit && <Textarea.EditButton onClick={handleEditToggleClick} />}
{!isEdit && <Textarea.CompleteButton onClick={handleSubmit(onSubmit)} />}
<Textarea.DeleteButton onClick={handleDeletePostClick} />
</Textarea>
);
}
위 코드를 보면 편지 작성 컴포넌트와 댓글 작성 컴포넌트가 height만 다르고 동일한 구조를 사용하고 있고, 이전 포스트 컴포넌트만 버튼이 추가되어 있는 걸 볼 수 있는데 이런 식으로 합성 컴포넌트 패턴을 사용하면 사용하는 입장에서 서브 컴포넌트를 마음대로 합성하여 사용할 수 있기에 유연성이 확장되고, 이 공통 컴포넌트를 사용하는 다른 개발자로 하여금 편리함을 제공할 수 있다.
현재까지의 코드를 보며 개인적인 생각으로는 각각의 컴포넌트에 들어가는 props가 많아 가독성을 해치고 있다고 생각이 드는데, props를 줄일 수 있는 방법을 고민해 보면 좋을 것 같다. 추가로 위의 코드 중 컴포넌트(함수)에 속성을 추가하는 로직은 아래처럼 작성하여 가독성을 조금 높일 수 있지 않을까 싶다.
// 이전 코드
Textarea.TextareaTitle = TextareaTitle;
Textarea.TextareaContent = TextareaContent;
Textarea.TextareaUnderLine = TextareaUnderLine;
Textarea.CompleteButton = CompleteButton;
Textarea.EditButton = EditButton;
Textarea.DeleteButton = DeleteButton;
// 개선된 코드
Object.assign(Textarea, {
TextareaTitle,
TextareaContent,
TextareaUnderLine,
CompleteButton,
EditButton,
DeleteButton
});
'React' 카테고리의 다른 글
렌더링을 빨리 빨리!! by 번들 사이즈 최적화 (2) | 2024.04.25 |
---|---|
프론트엔드 성능 최적화(Image 압축 및 확장자 변경 자동화) (0) | 2024.04.13 |
프론트엔드 성능 최적화(Font) (0) | 2024.04.12 |
human error를 줄여 동료 개발자의 경험을 개선해보자! with console.log() (0) | 2024.04.08 |
OAuth 로그인 구현기 (2) | 2024.04.04 |
같은 실수를 반복하지 않고, 한 번 학습한 내용을 오래 기억하기 위해 개발하면서 겪었던 트러블 슈팅과 학습한 내용을 정리하고 기록합니다 🧑💻