이 포스트는 이 글을 번역 및 정리한 내용입니다.
리액트를 사용하는 모든 개발자들은 아마도 스스로에게 이런 질문들을 이미 던져봤을 것입니다.
- 어떻게 다양한 케이스에 맞는 재사용 가능한 컴포넌트를 만들까?
- 어떻게 사용하기 쉬운 간단한 API를 가진 컴포넌트를 만들까?
- 어떻게 UI나 기능 측면에서 확장 가능한 컴포넌트를 만들까?
이런 질문이 반복되면서 리액트 커뮤니티에서 몇 가지 고급 패턴이 생겨났습니다.
1. Compound Components Pattern - 합성 컴포넌트 패턴
이 패턴을 사용하면 불필요한 prop drilling 없이 표현적이고 선억적인 컴포넌트를 만들 수 있습니다. 컴포넌트를 합성하여 커스터마이징해서 사용할 수 있도록 하려면 이 패턴을 사용하는 것을 고려해 보는 것이 좋습니다.
용어 설명
prop drilling : 하위 요소에서 사용 하기 위해 상위 요소에서 프롭을 넘겨주는 것. 전역 변수 보다 이를 선호하는 것은 이 값이 사용되는 위치가 더욱 명확하여 추적이 쉽고, 변경 사항이 어플리케이션에 미치는 영향을 결정하는 프로세스를 쉽게 해주기 때문이다. 그러나 어플리케이션이 커지면서 여러 계층을 통해 프롭을 드릴하게 되면 추적이 어려워진다.
예시,
import React from "react";
import { Counter } from "./Counter";
function Usage() {
const handleChangeCounter = (count) => {
console.log("count", count);
};
return (
<Counter onChange={handleChangeCounter}>
<Counter.Decrement icon="minus" />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon="plus" />
</Counter>
);
}
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/compound-component
장점
감소된 API 복잡성
하나의 거대한 부모 컴포넌트에서 모든 프롭스를 물려주는 대신, 각각의 프롭스들이 명확히 하위 컴포넌트에 연결됩니다.
유연한 마크업 구조
개발자가 컴포넌트의 순서를 변경할 수 있습니다.
명확한 관심 분리
Counter : 주요 로직 담당
CounterProvier : state와 event hnadlers를 제공
userCounterContext : state와 event handlers를 사용
단점
너무 많은 UI 유연성
예기치 못한 요소의 추가, 순서를 잘못 지정, 필수 컴포넌트를 포함하지 않는 등 내가 만들어둔 컴포넌트를 다른 개발자가 잘못 사용할 가능성이 있습니다.
코드 수의 증가
이 패턴을 사용하는 라이브러리,
2. Control Props Pattern - 프롭스를 제어하는 패턴
이 패턴은 어떤 개발자(이 컴포넌트를 처음 개발한 사람)가 만들어둔 컴포넌트를 제어 가능하게 변형합니다. 외부 state를 통해 내부에 프롭스를 제공해 기본적으로 제공했던 컴포넌트의 동작 이외에 커스텀 로직을 주입할 수 있습니다.
import React, { useState } from "react";
import { Counter } from "./Counter";
function Usage() {
const [count, setCount] = useState(0);
const handleChangeCounter = (newCount) => {
setCount(newCount);
};
return (
<Counter value={count} onChange={handleChangeCounter}>
<Counter.Decrement icon={"minus"} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count max={10} />
<Counter.Increment icon={"plus"} />
</Counter>
);
}
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/control-props
장점
더 많은 제어 가능
컴포넌트 외부로 state가 나와 있기 때문에, 개발자가 그것을 컨트롤하고 컴포넌트에 영향을 끼칠 수 있음
단점
구현 복잡
이전 패턴에 비해 신경써야할 포인트가 더 많아 졌다.
이 패턴을 사용하는 라이브러리,
3. Custom Hook Pattern
주요 로직은 커스텀 훅으로 넘겨줍니다. 이 커스텀 훅은 이 컴포넌트를 사용하는 개발자가 접근 가능하고, 더 많은 제어를 가능하게 합니다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
function Usage() {
const { count, handleIncrement, handleDecrement } = useCounter(0);
const MAX_COUNT = 10;
const handleClickIncrement = () => {
//Put your custom logic
if (count < MAX_COUNT) {
handleIncrement();
}
};
return (
<>
<Counter value={count}>
<Counter.Decrement
icon={"minus"}
onClick={handleDecrement}
disabled={count === 0}
/>
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment
icon={"plus"}
onClick={handleClickIncrement}
disabled={count === MAX_COUNT}
/>
</Counter>
<button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}
export { Usage };
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/custom-hooks
장점
더 많은 제어 가능
이 컴포넌트를 사용하려는 개발자에게 자신의 로직을 주입 가능하게 하여, 컴포넌트의 기본 행동을 수정하게 허락합니다.
단점
구현 복잡성
로직이 렌더링 부분과 분리되어 있기 때문에 개발자가 이 컴포넌트를 사용하려면 이 컴포넌트가 어떻게 동작하는 지 잘 이해해야 합니다.
이 패턴을 사용하는 라이브러리,
4. Props Getters Pattern
Custom hook pattern이 훌륭한 제어를 제공하지만, 개발자가 자신의 로직을 다시 만들어야 하기 때문에 컴포넌트를 통합하기 어렵게 만듭니다. (대충 넘 제각각으로 사용한다는 의미) Props Getters Pattern을 이 복잡성을 숨기려고 합니다. 기본 프롭스를 노출하지 않고, Prps getters를 제공합니다. 이 getter는 많은 프롭스들을 리턴하는 기능을 합니다. 이 프롭스들은 개발자가 자연스럽게 적절한 JSX요소에 프롭스들을 연결할 수 있도록 의미있는 이름을 가지고 있습니다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
const MAX_COUNT = 10;
function Usage() {
const {
count,
getCounterProps,
getIncrementProps,
getDecrementProps
} = useCounter({
initial: 0,
max: MAX_COUNT
});
const handleBtn1Clicked = () => {
console.log("btn 1 clicked");
};
return (
<>
<Counter {...getCounterProps()}>
<Counter.Decrement icon={"minus"} {...getDecrementProps()} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} {...getIncrementProps()} />
</Counter>
<button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
Custom increment btn 1
</button>
<button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
Custom increment btn 2
</button>
</>
);
}
export { Usage };
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/props-getters
장점
사용하기 쉽다
복잡성은 숨기고, 개발자에게 적절한 프롭스를 적절한 JSX요소에 넘겨주게 만듭니다.
유연성
개발자가 프롭스를 오버로딩할 수 있습니다.
단점
가시성 부족
추상화로 인해 컴포넌트를 통합할 수 는 있지만, 컴포넌트를 올바르게 재사용하려면 개발자가 내부 논리를 알아야 합니다.
이 패턴을 사용하는 라이브러리,
5. State reducer pattern
이 패턴은 개발자가 컴포넌트 내부의 동작 방식을 변경할 수 있는 방법을 제공합니다. Custom Hook Pattern과 유사하지만 개발자가 추가로 reducer 를 정의합니다. 이 reducer 는 해당 컴포넌트의 어떤 내부 액션도 오버 로드합니다.
import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";
const MAX_COUNT = 10;
function Usage() {
const reducer = (state, action) => {
switch (action.type) {
case "decrement":
return {
count: Math.max(0, state.count - 2) //The decrement delta was changed for 2 (Default is 1)
};
default:
return useCounter.reducer(state, action);
}
};
const { count, handleDecrement, handleIncrement } = useCounter(
{ initial: 0, max: 10 },
reducer
);
return (
<>
<Counter value={count}>
<Counter.Decrement icon={"minus"} onClick={handleDecrement} />
<Counter.Label>Counter</Counter.Label>
<Counter.Count />
<Counter.Increment icon={"plus"} onClick={handleIncrement} />
</Counter>
<button onClick={handleIncrement} disabled={count === MAX_COUNT}>
Custom increment btn 1
</button>
</>
);
}
export { Usage };
Github: https://github.com/alexis-regnaud/advanced-react-patterns/tree/main/src/patterns/state-reducer
장점
더 많은 제어 가능
복잡한 컴포넌트의 경우 reducer를 사용해 개발자에게 제어권을 맡기는 것이 좋은 방법입니다. 컴포넌트의 모든 내부의 작업은 이 패턴에서 외부에서 접근하고 재정의할 수 있습니다.
단점
구현 복잡성
구현하기 복잡합니다.
가시성 부족
내부 동작 논리에 대한 충분한 이해가 필요합니다.
이 패턴을 사용하는 라이브러리,
결론
큰 힘에는 큰 책임이 따른다.
제어권을 다른 개발자에게 많이 넘겨줄 수록 컴포넌트가 "plug & play" (즉시 시작)이라는 사고 방식에서 멀어집니다.
시의적절하게 패턴을 선택하는 것은 개발자의 역할입니다.