React
로 여러 컴포넌트를 만들다 보면 이를 재사용하기 props
를 추가하면서 처음 계획가 달리 컴포넌트의 구조가 복잡해지는 경우가 있었습니다. 이로 인해 코드의 가독성이 떨어지거나 새로운 컴포넌트를 만들게 되었습니다.
위와 같은 상황에서 컴파운드 컴포넌트 패턴(Compound Components Pattern)를 사용하여 해결이 가능하여 해당 글을 기록합니다.
컴파운드 컴포넌트은 headless component로 기능은 있지만 스타일이 없는 컴포넌트입니다.
prop
를 사용하지 않고 내부에서 데이터를 처리할 수 있고, 비슷한 디자인의 컴포넌트가 필요할 때 새로운 컴포넌트를 만들지 않고 사용할 수 있습니다. 이를 통해 유연하고 재사용 가능한 컴포넌트를 설계할 수 있고, 가독성과 유지 보수성을 높일 수 있습니다.
컴포넌트 내부에서 데이터를 처리하기 위해 Context API를 사용합니다.
일반적인 체크박스를 만들게 된다면 아래와 같은 코드를 가지게됩니다.
import { useState } from 'react'
const Checkbox = () => {
const [isChecked, setIsChecked] = useState(false)
return (
<label>
<input
type="checkbox"
checked={isChecked}
onChange={() => setIsChecked(!isChecked)}
/>
<span>체크박스 만들기</span>
</label>
)
}
export default Checkbox
이를 컴포넌트로 만들어서 다른 곳에서 사용하기 위해서는 어떤 내용을 체크하는 지에 대한 라벨, 체크가 되었는지의 상태 값, 체크하는 로직을 props
를 받아야 합니다. 수정한다면 아래와 같은 형태가 될 것입니다.
type CheckboxProps = {
label: string
isChecked: boolean
onChange: () => void
}
const Checkbox = ({ label, isChecked, onChange }: CheckboxProps) => {
return (
<label>
<input type="checkbox" checked={isChecked} onChange={onChange} />
<span>{label}</span>
</label>
)
}
export default Checkbox
export default function App() {
const [isChecked, setIsChecked] = useState(false)
return (
<Checkbox
label="체크박스 만들기"
isChecked={isChecked}
onChange={() => setIsChecked(!isChecked)}
/>
)
}
만약 체크박스를 사용하는 모든 곳에서 디자인과 기능이 동일하다면 이대로 사용해도 문제없지만,
특정 페이지들에서만 색상을 다르게 한다던지, 모바일에서는 체크박스를 오른쪽으로 옮겨야 한다던지 등의 레이아웃의 변경이 필요하다면 상황이 곤란해집니다.
import * as React from 'react'
type CheckboxContextProps = {
id: string
isChecked: boolean
onChange: () => void
}
type CheckboxProps = CheckboxContextProps & React.PropsWithChildren<{}>
const CheckboxContext = React.createContext<CheckboxContextProps>({
id: '',
isChecked: false,
onChange: () => {},
})
const CheckboxWrapper = ({
id,
isChecked,
onChange,
children,
}: CheckboxProps) => {
const value = {
id,
isChecked,
onChange,
}
return (
<CheckboxContext.Provider value={value}>
{children}
</CheckboxContext.Provider>
)
}
const useCheckboxContext = () => {
const context = React.useContext(CheckboxContext)
return context
}
const Checkbox = ({ ...props }) => {
const { id, isChecked, onChange } = useCheckboxContext()
return (
<input
type="checkbox"
id={id}
checked={isChecked}
onChange={onChange}
{...props}
/>
)
}
const Label = ({ children, ...props }: React.PropsWithChildren<{}>) => {
const { id } = useCheckboxContext()
return (
<label htmlFor={id} {...props}>
{children}
</label>
)
}
CheckboxWrapper.Checkbox = Checkbox
CheckboxWrapper.Label = Label
export default CheckboxWrapper
import CheckboxWrapper from './CheckboxWrapper'
export default function App() {
const [isChecked, setIsChecked] = useState(false)
return (
<CheckboxWrapper
id="checkbox-1"
isChecked={isChecked}
onChange={() => setIsChecked(!isChecked)}
>
<CheckboxWrapper.Checkbox />
<CheckboxWrapper.Label>체크박스 만들기</CheckboxWrapper.Label>
</CheckboxWrapper>
)
}
컴포넌트 내부에서 state를 공유하기 위해 Context API를 사용해서 처음에 작성해야 하는 코드가 꽤 많지만, 컴포넌트를 사용하는 곳에서는 하위에 어떤 컴포넌트가 있는지 볼 수 있고, 위치도 자유롭게 수정 가능합니다.
React에서의 컴포넌트는 재사용이 중요하다고 생각하여 지금까지 무분별하게 많은 props를 사용하거나 복잡하게 코드를 작성했었습니다. 어떻게든 코드를 집어넣으며 컴포넌트를 만들었더니 오히려 코드의 가독성을 가독성대로 떨어지고 컴포넌트를 사용할 때도 다른 사람들이 보기에 이해하기 어려운 조건으로 가득 찬 컴포넌트를 만들었습니다. 앞으론 적절한 상황에서 컴파운드 컴포넌트를 통해 코드를 작성한다면 React를 더욱 효과적으로 활용할 수 있을 거 같습니다.