안녕하세요! RyuWoong 입니다.
드디어 React에서 10번째 포스팅입니다. 와!
React에서 조금 핵심적인 부분들을 주로 포스팅 했는데,
사실 앞선 모든 내용들이 이 Compound Component 를 위한 빌드업 이였다는 것!
보고나면 오호라! 하실 내용이니 한번 가 봅시다!
Compound Component?
합성 컴포넌트? 이건 뭘까요?
React에 어느정도 눈을 뜬 개발자라면, Component를 만들어 잘 사용하고 계실 꺼라 생각합니다.
하지만 Component들이 기획에 따라, 디자인에 따라 계속 변경될 수 있겠죠? 만약 이 Component가 두루 재사용하고 있던 Component라면 새로운 Component를 만들어야하나 고민하게 될 것입니다.
문제.
React 새내기 분들은 아래와 같은 느낌으로 Component를 많이 만들었을거라 생각합니다. 저 또한 많이 그랬구요.
<Counter title="카운터" initValue={0} minimum={0} maximum={100} />
하지만 실무를 행하다보면, 기획과 디자인이 변경되는 경우가 잦습니다.
타이틀을 좀 더 다양하게 ... , 버튼도.. , 레이아웃이 ...
이런 이야기를 듣다보면 어떻게 Component를 만들어야 할까 고민이 됩니다. 🤔
이런 다양한 문제를 해결하기 위해 디자인 패턴이라는 것이 있는데요.
Compound Component도 디자인 패턴 중 하나입니다.
해결방법.
<Select>
<Option />
<Option />
<Option />
</Select>
HTML에서 Select Box를 만들때 위 코드처럼 구성하게 될 겁니다.
어떤가요? 명확하게 어떤 구성요소가 있는지 확인 할 수 있고, 커스텀도 쉽습니다.
이러한 디자인을 착안한 것이 Compound Component입니다.
사용하기.
앞서 보여드렸던 Counter를 Compound Component로 만들어 보겠습니다.
<Counter initValue={0} minimum={0} maximum={100}>
<Counter.Title>카운터</Counter.Title>
<Counter.Group>
<Counter.Button type="decrement">-</Counter.Button>
<Counter.Status />
<Counter.Button type="increment">+</Counter.Button>
</Counter.Group>
</Counter>
이제 기존보다 더 유동적으로 대처가 가능해졌습니다!
또한 Counter가 어떻게 구성되어 있는지 추론하기도 쉬워졌습니다.
내부는 어떻게 동작하는지 보여드리겠습니다.
먼저, Counter Component입니다.
// Components/Counter/Counter.tsx
import { createContext, useState } from 'react';
import CounterButton from './button';
import CounterGroup from './group';
import CounterStatus from './status';
import CounterTitle from './title';
interface Props {
children: React.ReactNode;
initValue?: number;
minimum?: number;
maximum?: number;
}
export const CounterContext = createContext({ value: 0, increment: () => {}, decrement: () => {} });
function Counter({ children, initValue = 0, minimum, maximum }: Props) {
const [count, setCount] = useState(initValue);
const increment = () =>
setCount((prev) => {
if (maximum === undefined) {
return prev + 1;
} else {
return prev < maximum ? prev + 1 : prev;
}
});
const decrement = () =>
setCount((prev) => {
if (minimum === undefined) {
return prev - 1;
} else {
return prev > minimum ? prev - 1 : prev;
}
});
return (
<CounterContext.Provider value={{ value: count, increment, decrement }}>
<div>{children}</div>
</CounterContext.Provider>
);
}
Counter.Title = CounterTitle;
Counter.Group = CounterGroup;
Counter.Button = CounterButton;
Counter.Status = CounterStatus;
export default Counter;
Counter Component는 Counter의 부모의 역할을 하는 Component입니다.
이곳에서 상태를 관리하며 내부에 상태를 전파해주기 위해 Context API를 사용했습니다.
그리고 children을 사용했다는 점을 눈여겨 보시면 좋을 거 같습니다.
아래는 자식 Component들 입니다.
// components/counter/title.tsx
import { css } from '@emotion/react';
interface Props {
children: React.ReactNode;
}
function CounterTitle({ children }: Props) {
return <h2 css={Title}>{children}</h2>;
}
const Title = css({ textAlign: 'center' });
export default CounterTitle;
// components/counter/status.tsx
import { css } from '@emotion/react';
import useCounter from './hook/useCounter';
function CounterStatus() {
const { value } = useCounter();
return <p css={Count}>{value}</p>;
}
const Count = css({
display: 'inline-block',
textAlign: 'center',
width: 40,
padding: 5,
fontWeight: 'bold',
});
export default CounterStatus;
// components/counter/button.tsx
import { css } from '@emotion/react';
import useCounter from './hook/useCounter';
interface Props {
children: React.ReactNode;
type: 'increment' | 'decrement';
}
function CounterButton({ children, type }: Props) {
const { increment, decrement } = useCounter();
return (
<button css={Box} onClick={type === 'increment' ? increment : decrement}>
{children}
</button>
);
}
const Box = css({
padding: 5,
width: 30,
border: '1px solid #000',
cursor: 'pointer',
});
export default CounterButton;
보시면 useCounter 라는 CustomHook을 사용하고 있습니다.
// components/counter/hook/useCounter.ts
import { useContext } from 'react';
import { CounterContext } from '..';
function useCounter() {
const value = useContext(CounterContext);
if(!value) {
throw Error('Conuter Context가 없습니다.')
}
return value;
}
export default useCounter;
왜 사용했을까요? 우선 다양한 곳에서 반복해서 사용할 수 있으며, 지금은 예제 코드여서 아주 단순한 코드지만 만약 실 서비스 코드들에서는 복잡한 로직(이전 Custom Hook 예제 코드처럼)을 대체해주기 때문에 UI만 집중해서 볼 수 있도록 관심사를 분리 할 수 있습니다.
관련된 자세한 내용은 Toss Slash, FEConf Korea에서 확인하실 수 있습니다! (진짜 좋은 영상입니다. 꼭 한번 보시길 추천드립니다.)
이제 다른 곳에서 Counter를 사용해야할때,
- - 타이틀을 변경, 제거 하고 싶어요.
- - 와 + 버튼을 변경하고 싶어요.
- Group화 시킨 순서를 변경 하고 싶어요.
위처럼 다양한 요구사항에 좀 더 폭 넓게 대응할 수 있습니다.
쉽게 설명 드리려고 했지만 쉽지 않은 것 같습니다. 제 설명은 참조만 하시고 참조에 걸린 내용들을 보시는 것이 더 도움이 될 것 같네요!
참조.
'Front-End > React' 카테고리의 다른 글
[React] Component 모듈화 .11 (0) | 2023.02.26 |
---|---|
[React] Custom Hook .09 (0) | 2023.02.14 |
[React] Context API .08 (0) | 2023.02.05 |
[React] Hooks - useReducer .07 (0) | 2023.02.03 |
[React.js] Hooks - useRef .06 (0) | 2021.01.20 |
삽질의 기록과 일상을 남기는 블로그입니다. 주로 React Native를 다룹니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!