참고한 사이트
- https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6
- https://www.patterns.dev/posts/compound-pattern/
- https://velog.io/@dnr6054/%EC%9C%A0%EC%9A%A9%ED%95%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%ED%8C%A8%ED%84%B4-5%EA%B0%80%EC%A7%80
- https://www.youtube.com/watch?v=aAs36UeLnTg&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
✨ Compound Components Pattern
장점: 대부분의 로직이 부모 컴포넌트에 존재합니다. state와 handlers는 context를 통해 공유하기 때문에, 불필요한 prop drilling이 없고, UI를 쉽게 커스터마이징 할 수 있습니다.
단점: UI의 높은 유연성 때문에, 필수 자식 컴포넌트의 누락 등 예상하지 못한 결과가 발생할 수 있고, JSX 행이 길어질 수 있습니다.
🚩 구현
1. context API를 통해서, Provider와 useCounterContext 정의
src/patterns/compoundComponent/useCounterContext.jsx
import { createContext, useContext } from 'react';
const CounterContext = createContext(null);
export function CounterProvider({ children, value }) {
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// 자식 컴포넌트에서 state와 handler 공유
export function useCounterContext() {
return useContext(CounterContext);
}
2. 부모 컴포넌트(Counter)에서 공유 state와 handler 정의하고, 자식 컴포넌트(Decrement, Increment, Lable, Count)와 부모 컴포넌트의 관계를 연결.
src/patterns/compoundComponent/Counter.jsx
import { useState } from 'react';
import Count from './components/Count';
import Decrement from './components/Decrement';
import Increment from './components/Increment';
import Label from './components/Label';
import { CounterProvider } from './useCounterContext';
export default function Counter({ children }) {
const [num, setNum] = useState(0);
const incrementNum = () => setNum((prev) => prev + 1);
const decrementNum = () => setNum((prev) => prev - 1);
return (
<CounterProvider value={{ num, incrementNum, decrementNum }}>
{children}
</CounterProvider>
);
}
Counter.Decrement = Decrement;
Counter.Increment = Increment;
Counter.Label = Label;
Counter.Count = Count;
src/patterns/compoundComponent/components/Count.jsx
src/patterns/compoundComponent/components/Decrement.jsx
src/patterns/compoundComponent/components/Increment.jsx
src/patterns/compoundComponent/components/Label.jsx
import React from 'react';
import { useCounterContext } from '../useCounterContext';
export default function Count() {
const { num } = useCounterContext();
return <strong style={{ padding: '4px' }}>{num}</strong>;
}
import React from 'react';
import { useCounterContext } from '../useCounterContext';
export default function Decrement() {
const { decrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={decrementNum}>
-
</button>
);
}
import React from 'react';
import { useCounterContext } from '../useCounterContext';
export default function Increment() {
const { incrementNum } = useCounterContext();
return (
<button type="button" style={{ padding: '4px' }} onClick={incrementNum}>
+
</button>
);
}
import React from 'react';
export default function Label({ children }) {
return <span style={{ padding: '4px' }}>{children}</span>;
}
3. Counter 컴포넌트 사용
src/patterns/compoundComponent/Cart.jsx
import React from 'react';
import Counter from './Counter';
export default function Cart() {
return (
<>
<Counter>
<Counter.Decrement />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment />
</Counter>
<hr />
<Counter>
<Counter.Decrement />
<Counter.Increment />
<Counter.Label>Counter2</Counter.Label>
<Counter.Count />
</Counter>
<hr />
<Counter>
<Counter.Label>Counter3</Counter.Label>
<Counter.Increment />
<Counter.Count />
</Counter>
</>
);
}