Render Props 패턴
JSX 엘리먼트를 props를 통해 컴포넌트에게 전달한다
📜 원문: patterns.dev - render props pattern
📜 번역: https://patterns-dev-kr.github.io/design-patterns/render-props-pattern/
고차 컴포넌트 섹션에서 여러 컴포넌트가 동일한 데이터나 동일한 로직을 포함해야 할 때 컴포넌트의 로직을 재사용할 수
있게 되면 편해질 수 있다는 것을 알았습니다.
컴포넌트를 재사용 가능하게 할 수 있는 또 다른 방법으로, render prop 패턴을 사용하는 방법이 있습니다.
render prop은 컴포넌트의 prop으로 함수이며 JSX 엘리먼트를 리턴합니다. 컴포넌트 자체는 아무런 것도 렌더링하지 않지
만 render prop함수를 호출하죠.
Title컴포넌트가 있다고 생각해 볼까요? Title컴포넌트는 prop으로 넘어온 함수를 호출하여 반환하는 것 외에는 아무런
동작을 하지 않습니다. Title컴포넌트에 render prop을 아래와 같이 넣어 봅시다.
<Title render={() => <h1>I am a render prop!</h1>} />
Title컴포넌트 내에서는 단순히 prop의 render함수를 호출하여 반환합니다.
const Title = props => props.render()
컴포넌트 엘리먼트에 React엘리먼트를 반환하는 render라는 이름의 prop을 넘깁니다.
render prop패턴의 장점은 prop을 받는 컴포넌트가 재사용성이 좋다는 점입니다.
Title컴포넌트는 이제 render prop만 바꿔가며 여러번 사용할 수 있죠.
이 패턴의 이름이 render prop이지만 넘기는 prop의 이름을 꼭 render로 할 필요는 없습니다.
JSX를 렌더하는 어떤 prop이던 render prop으로 볼 수 있습니다. 아래 예제에서는 이름을 변경하여 사용하고 있죠.
위의 예제에서 render prop패턴을 사용하여 컴포넌트를 재사용 가능하게 만들었습니다.
하지만 이 패턴은 이보다 더 유용하게 쓰일 수 있죠.
render prop을 받는 컴포넌트는 단순히 함수를 호출해 JSX엘리먼트를 렌더링하는 것 외에도 많은 동작을 할 수 있습니다.
단지 함수를 호출하는 것 대신에 render prop 함수를 호출할 때 인자를 전달할 수 있습니다.
function Component(props) {
const data = { ... }
return props.render(data)
}
위처럼 인자를 넘기게 구현하면 render prop은 이제 아래 코드와 같이 데이터를 인자로 받을 수 있습니다.
<Component render={data => <ChildComponent data={data} />} />
아래 예제는 텍스트박스에 섭씨 온도를 받아서 켈빈과 화씨 온도로 표현해주는 단순한 앱입니다.
위의 예제를 보면 Input 컴포넌트는 값 입력을 받기 위해 state를 갖고 있는데 Fahrenheit컴포넌트와 Kelvin 컴포넌트는
이 state를 전달 받을 방법이 없습니다.
상태를 부모 컴포넌트로 올리기
Fahrenheit컴포넌트와 Kelvin 컴포넌트가 사용자가 입력한 값을 전달받기 위한 방법 중 하나는 상태를 부모 컴포넌트로 올려보내는 방법이 있습니다.
아래 예제에서 상태를 가지고 있는 Input 컴포넌트가 있지만 형제 컴포넌트인 Fahrenheit, Kelvin 컴포넌트도 이 값에 접근
할 수 있어야 변환된 값을 보여줄 수 있습니다. 이 때 Input 자체가 상태를 갖는 것 대신 세 컴포넌트의 부모 컴포넌트로
상태를 올려보내는 것인데 아래 예제에서는 App 컴포넌트가 될 것입니다.
function Input({ value, handleChange }) {
return <input value={value} onChange={e => handleChange(e.target.value)} />
}
export default function App() {
const [value, setValue] = useState('')
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input value={value} handleChange={setValue} />
<Kelvin value={value} />
<Fahrenheit value={value} />
</div>
)
}
이 방법도 유효하긴 하지만 규모가 큰 앱에서 컴포넌트가 여러 자식 컴포넌트를 가지고 있는 경우 이 작업을 하기란 까다로
운 일입니다. 상태의 변경은 모든 자식 컴포넌트의 리렌더링을 유발할 수 있고 이런 상황이 쌓이면 앱의 전체적인 성능을
떨어트릴 수 있습니다.
Render props
그 대신에 render props 패턴을 활용할 수 있다. Input 컴포넌트가 render prop을 받도록 리펙토링 해 보죠.
function Input(props) {
const [value, setValue] = useState('')
return (
<>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.render(value)}
</>
)
}
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input
render={value => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
)
}
이로써 Kelvin과 Fahrenheight 컴포넌트는 사용자의 입력 값을 받을 수 있게 되었습니다.
자식 컴포넌트를 함수로 받아보자
일반적인 JSX컴포넌트에 자식 엘리먼트로 React 엘리먼트를 반환하는 함수를 전달할 수 있습니다.
해당 컴포넌트에서 이 함수는 children prop으로 사용 가능하며 이것도 역시 render prop에 해당합니다.
Input 컴포넌트에 명시적으로 render prop을 넘기는 대신 자식 컴포넌트를 함수로 넘기도록 수정해 보죠.
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input>
{value => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
</Input>
</div>
)
}
Input컴포넌트는 props.children을 통해 이 함수에 접글할 수 있습니다. props.render를 쓰는 대신에 props.children함수
를 호출하며 인자를 넘기도록 수정합니다.
function Input(props) {
const [value, setValue] = useState('')
return (
<>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.children(value)}
</>
)
}
이렇게 하여 render prop의 이름을 어떻게 지을까 고민하지 않고 Kelvin과 Fahrenheit 둘 다 사용자의 입력 값에 접근할 수
있습니다.
Hooks
몇몇 상황에 render props 패턴은 hooks로 대체될 수 있다. Apollo Client가 좋은 예시입니다.
아래 예제 코드를 이해하는데 Apollo Client에 대한 지식은 필요하지 않습니다.
Apollo Client를 사용하는 방법 중 하나는 Mutation과 Query 컴포넌트를 사용하는 것입니다.
아래 예시는 HOC Pattern 의 Input컴포넌트 예시와 동일한데 graphql() HOC를 사용하는 대신 Mutation 컴포넌트가
render prop을 받는것을 알 수 있습니다.
Mutation컴포넌트가 자식 엘리먼트에게 데이터를 전달할 수 있도록 하기 위해 컴포넌트를 렌더하는 함수를 자식 요소로
제공했습니다. 이 함수에서 인자로 데이터를 받을 수 있죠.
<Mutation mutation={...} variables={...}>
{addMessage => <div className="input-row">...</div>}
</Mutation>
render prop 형태는 HOC에 비교하여 조금 더 선호되긴 하지만 단점이 존재합니다.
첫번째 단점은 트리가 깊어진다는 것입니다. 컴포넌트가 여러 개의 mutation을 사용해야 하는 경우 Mutation 컴포넌트나
Query 컴포넌트를 중첩해 사용해야 합니다.
<Mutation mutation={FIRST_MUTATION}>
{firstMutation => (
<Mutation mutation={SECOND_MUTATION}>
{secondMutation => (
<Mutation mutation={THIRD_MUTATION}>
{thirdMutation => (
<Element
firstMutation={firstMutation}
secondMutation={secondMutation}
thirdMutation={thirdMutation}
/>
)}
</Mutation>
)}
</Mutation>
)}
</Mutation>
React에 훅이 추가되고 나서 Apollo에도 훅을 지원하기 시작했습니다.
Mutation 혹은 Query 컴포넌트를 사용하는 대신 개발자는 훅을 사용하여 직접 필요한 값을 참조할 수 있게 되었습니다.
아래 예시에서는 Query 컴포넌트를 render prop과 함께 사용하는 대신 useQuery훅을 사용하고 있습니다.
useQuery훅을 사용하여 꽤 많은 양의 코드를 줄이면서 필요한 데이터를 사용할 수 있게 되었습니다.
장점
render prop을 사용하여 몇몇 컴포넌트간 데이터를 공유하는것은 간단합니다.
children prop을 활용하는 것으로 해당 컴포넌트를 재사용할 수 있게 됩니다.
HOC패턴도 마찬가지로 재사용성과 데이터의 공유 부분에서 같은 이슈를 해결할 수 있습니다. render prop은 HOC를 사용할 때 마주칠 수 있는 몇 가지 이슈들을 해결할 수 있죠.
props를 자동으로 머지하도록 구현하지 않기 때문에 HOC패턴을 사용할 때 prop이 어디서 만들어져 어디서 오는지 구별하기 힘들었던 이슈가 없습니다. 부모 컴포넌트로부터 받은 prop을 명시적으로 받아 처리하기 때문이죠.
함수의 인자에서 명시적으로 prop이 전달되기 때문에 HOC를 사용할 때 prop이 모호한 문제가 해결됩니다.
이 덕분에 prop이 어디로부터 오는지 확실히 알 수 있죠.
render props를 활용하여 렌더링 컴포넌트와 앱의 로직을 분리할 수 있습니다. 상태를 가진 컴포넌트는 render prop을
받고. 상태가 없는 컴포넌트를 렌더할 수 있습니다.
단점
위에서 render props로 해결하려 한 문제는 React hooks로 대체되었습니다. Hooks는 컴포넌트에 재사용성과 데이터 공유를 위한
방법 자체를 바꿔놓았죠. 대부분의 render props는 Hooks로 대체 가능합니다.
render prop 내에서는 생명 주기 함수를 사용할 수 없기 때문에 render prop 패턴은 받은 데이터를 수정할 필요가 없는 컴포넌트들
에 대하여 사용할 수 있습니다.
참조
'프로그래밍(Basic) > 디자인 패턴(JS)' 카테고리의 다른 글
[바미] Compound 패턴 (1) | 2022.10.07 |
---|---|
[바미] - Hooks 패턴 (2) | 2022.10.04 |
[바미] HOC 패턴 (0) | 2022.09.24 |
[바미] Mediator/Middleware 패턴 (0) | 2022.09.23 |
[바미] Mixin 패턴 (0) | 2022.09.22 |