๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

FrontEnd

[๋ฒˆ์—ญ] ๋ฆฌ์•กํŠธ Compound Component ์ž˜ ์‚ฌ์šฉํ•˜๊ธฐ

๋ฐ˜์‘ํ˜•

์›๋ฌธ : https://itnext.io/unlock-the-full-potential-of-react-with-the-compound-components-pattern-495a1bdb8b8f

 

๐Ÿ”ฅ Best Practices of React Compound Components Pattern: Unlock the Full Potential

Let’s improve React Component Design with This Surprisingly Easy Pattern

itnext.io

Compound Component ํŒจํ„ด์ด๋ž€?

์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด(๋ณตํ•ฉ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด)์€ ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ž‘์€ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ํ•ฉ์„ฑํ•˜๋Š” ๊ฒƒ

  • ๋‚ด๋ถ€์ ์œผ๋กœ ์บก์Šํ™”๋œ ์ƒํƒœ๋ฅผ ๊ณต์œ 
  • ๋ณดํ†ต ์ปจํ…์ŠคํŠธ(์ƒํƒœ)๋ฅผ ๊ฐ€์ง„ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ, ์ƒํƒœ๋ฅผ ์ œ๊ณต๋ฐ›์•„ ์‚ฌ์šฉํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•˜๋Š” ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌ์„ฑ๋จ
<Modal>
  <Modal.Header>
    Confirm action
  </Modal.Header>
  <Modal.Body>
    Are you sure?
  </Modal.Body>
  <Modal.Footer>
    <button onClick={handleConfirm}>Confirm</button>
  </Modal.Footer>
</Modal>

Compound Component ๋ชจ๋ฒ” ์‚ฌ๋ก€

๋ชจ๋ฒ” ์‚ฌ๋ก€ 1:

์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๊ด€๊ณ„์™€ ๊ฒฐํ•ฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์‹ ์ค‘ํ•˜๊ฒŒ ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
์—ฌ๊ธฐ์—๋Š” ์ตœ์ƒ์˜ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ฐพ์„ ๋•Œ๊นŒ์ง€ ๋‹ค์–‘ํ•œ ๋ฐฐ์น˜ ๋ฐ ์ƒํ˜ธ ์ž‘์šฉ์„ ์‹คํ—˜ํ•˜๋Š” ๊ฒƒ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 2:

๋ฉ”๋‰ด ๋ฐ ํƒญ๊ณผ ๊ฐ™์€ ๋‹จ์ˆœํ•œ UI ์š”์†Œ์—์„œ ์–‘์‹ ๋ฐ ๋ฐ์ดํ„ฐ ์‹œ๊ฐํ™”์™€ ๊ฐ™์€ ๋ณด๋‹ค ๋ณต์žกํ•œ ์œ„์ ฏ์— ์ด๋ฅด๊ธฐ๊นŒ์ง€
๊ด‘๋ฒ”์œ„ํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•ต์‹ฌ์€ ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ์‹๋ณ„ํ•˜๊ณ  ๋” ์ž‘๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฌ์šด ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 3:

์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•๊ณผ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์˜ต์…˜์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ช…ํ™•ํ•œ API๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. 

๋ชจ๋ฒ” ์‚ฌ๋ก€ 4:

์ผ๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š๊ฑฐ๋‚˜ ์ž˜๋ชป ํ•ฉ์„ฑ๋˜๋”๋ผ๋„
์ „์ฒด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋„๋ก ๊ฐ ์ปดํฌ๋„ŒํŠธ์— ํ•ฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 5:

props๋ฅผ ํ†ตํ•ด ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. 
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด prop ๋“œ๋ฆด๋ง์ด ๋ฐœ์ƒํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์œ ์ง€ ๊ด€๋ฆฌํ•˜๊ธฐ๊ฐ€ ๋” ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๋Œ€์‹  Redux ๋˜๋Š” MobX์™€ ๊ฐ™์€ ์ปจํ…์ŠคํŠธ ๋˜๋Š” ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 6:

์ข…์ข… ์ปดํฌ๋„ŒํŠธ ๊ฐ„์— ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด React Context API์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 7:

shouldComponentUpdate, ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ๋˜๋Š” ์ฝ”๋“œ๋ฅผ ์ตœ์ ํ™”ํ•˜๋Š” ๊ธฐํƒ€ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜์—ฌ
๋ถˆํ•„์š”ํ•œ ๋ Œ๋”๋ง์„ ํ”ผํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋ฒ” ์‚ฌ๋ก€ 8:

"Render Props" ํŒจํ„ด ๋˜๋Š” "Higher-Order Components" ํŒจํ„ด๊ณผ ๊ฐ™์€
๋‹ค๋ฅธ ๋””์ž์ธ ํŒจํ„ด๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ํ›จ์”ฌ ๋” ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

React Compount Component ํŒจํ„ด ์„ค๊ณ„

๋จผ์ € ์ปดํฌ๋„ŒํŠธ์˜ ์ƒํƒœ๋ฅผ ํ†ตํ•ฉ๊ด€๋ฆฌ(์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜)ํ•˜๋Š” ์ตœ์ƒ์œ„ ์ปจํ…์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ณดํ†ต dot syntex๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

const Modal = ({ children }) => {
  const childrenArray = React.Children.toArray(children);
  const header = childrenArray.find(child => child.type === Modal.Header);
  const body = childrenArray.find(child => child.type === Modal.Body);
  const footer = childrenArray.find(child => child.type === Modal.Footer);

  return (
    <div className="modal">
      <div className="modal-header">{header}</div>
      <div className="modal-body">{body}</div>
      <div className="modal-footer">{footer}</div>
    </div>
  );
};

Modal.Header = ({ children }) => {
  return <div className="modal-header">{children}</div>;
};

Modal.Body = ({ children }) => {
  return <div className="modal-body">{children}</div>;
};

Modal.Footer = ({ children }) => {
  return <div className="modal-footer">{children}</div>;
};

์ด ์˜ˆ์ œ์—์„œ Modal ์ปดํฌ๋„ŒํŠธ๋Š” Modal.Header, Modal.Body ๋ฐ Modal.Footer ์„ธ ๊ฐ€์ง€ ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
์ด๋Ÿฌํ•œ ๊ฐ ์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ๋Š” Modal ์ปดํฌ๋„ŒํŠธ ๊ฐ์ฒด์˜ ์†์„ฑ์œผ๋กœ ์ •์˜๋ฉ๋‹ˆ๋‹ค.

 

Modal ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ๋ฉ”์„œ๋“œ์—์„œ
์„œ๋ธŒ ์ปดํฌ๋„ŒํŠธ๋Š” ์  ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ก์„ธ์Šคํ•˜๊ณ , ์ปดํฌ๋„ŒํŠธ์˜ ์ ์ ˆํ•œ ์œ„์น˜์— ๋ Œ๋”๋ง ํ•ฉ๋‹ˆ๋‹ค.

 

์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์„ค์ •ํ•˜๊ณ  ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ฆ‰ React ์ฝ”๋“œ๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๋Š” ๋ณด๋‹ค ์œ ์—ฐํ•˜๊ณ  ๋ชจ๋“ˆํ™”๋œ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋ณตํ•ฉ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์˜ ์ฃผ ๋‹จ์  ์ค‘ ํ•˜๋‚˜๋Š”,
๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ ์ง๊ณ„ ์ž์‹๋งŒ props์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
์ฆ‰, ์ด๋Ÿฌํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ž˜ํ•‘ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

Context API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณตํ•ฉ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ƒํƒœ ๊ณต์œ 

Context API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด
props๋ฅผ ํ†ตํ•œ ์ „๋‹ฌ ์—†์ด ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ƒํƒœ ๋˜๋Š” ์ •๋ณด๋ฅผ ์‰ฝ๊ฒŒ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

const ModalContext = createContext({});

const Modal = ({ children }) => {
  const [isOpened, setIsOpened] = useState(false)

  const childrenArray = React.Children.toArray(children);
  const header = childrenArray.find(child => child.type === Modal.Header);
  const body = childrenArray.find(child => child.type === Modal.Body);
  const footer = childrenArray.find(child => child.type === Modal.Footer);

  return (
    <ModalContext.Provider value={{ isOpened, setIsOpened }}>
      <div className="modal">
        <div className="modal-header">{header}</div>
        <div className="modal-body">{body}</div>
        <div className="modal-footer">{footer}</div>
      </div>
    </ModalContext.Provider>
  );
};

Modal.Header = ({ children }) => {
  const {setIsOpened} = useContext(ModalContext)
  return <div className="modal-header">{children}<button onClick={() => setIsOpened(false)}>X</button></div>;
};

Modal.Body = ({ children }) => {
  return <div className="modal-body">{children}</div>;
};

Modal.Footer = ({ children }) => {
  const {setIsOpened} = useContext(ModalContext)
  return <div className="modal-footer">{children}<button onClick={() => setIsOpened(false)}>Close</button></div>;
};โ€‹

ModalContext๋Š” createContext()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์„ฑ๋˜๋ฉฐ
useContext() ํ›„ํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Modal.Header ๋ฐ Modal.Footer ์ปดํฌ๋„ŒํŠธ์—

isOpened ์ƒํƒœ ๋ฐ setIsOpened ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

 

Header ์ปดํฌ๋„ŒํŠธ๋Š” ()=>setIsOpened(false) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋‹ซ๊ธฐ ๋ฒ„ํŠผ์„ ํฌํ•จํ•˜๋„๋ก ํ•˜์—ฌ
์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์„ ๋‹ซ์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.
๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์„ ๋‹ซ๋Š” ๋ฒ„ํŠผ์„ ํฌํ•จํ•˜๋„๋ก Footer ์ปดํฌ๋„ŒํŠธ๋„ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค.
 

๋ฆฌ์•กํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ๋กœ์ง๊ณผ ๋ Œ๋”๋ง ๋‘˜ ๋‹ค ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด, ๊ด€์‹ฌ์‚ฌ๊ฐ€ ์• ๋งคํ•  ์ˆ˜ ์žˆ์œผ๋‚˜,
์˜ต์…˜๊ณผ ์ƒํƒœ, ํ•จ์ˆ˜ ๋ชจ๋‘ ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ๊ด€์ ์—์„œ ์„ค๊ณ„ํ•˜๋ฉด ์ข‹๋‹ค.
์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ๋„ ์ตœ๋Œ€ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๋Š” ๋ชจ์Šต์„ ๋ฐ˜์˜ํ•˜๋ฉด ์ข‹๋‹ค.
๋ฆฌ์•กํŠธ๋ฅผ ์ด์šฉํ•œ ํ™”์ดํŠธํ…Œ์ŠคํŠธ(integration, e2e)์—์„œ๋„ ์œ ์‚ฌํ•œ ๋…ผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค

 

 

 

 

๋ฐ˜์‘ํ˜•