개인 공부를 위해 https://gist.github.com/bradwestfall/4fa683c8f4fcd781a38a8d623bec20e7를 번역한 글입니다.오역 의역 있을 수 있습니다.
원본 링크는 글 최하단에 기재해 놓았습니다.
누군가 저에게 HoC와 Render Props 패턴과 Hook의 비교를 요청했습니다. 다른사람들에게도 유용할수 있을거라 생각해 글을 공개해두겠습니다.
요약
HoC의 문제점들:
Render Props의 문제점들:
Hooks는 HoC와 Render Props가 가지고 있는 모든 문제점을 해결합니다.
우리가 우선적으로 이해하는 중점 포인트는 다음과 같습니다.
function MyComponent() {
return (
<div className="browse-users">
<div className="user-item">Name: Brad</div>
<div className="user-item">Name: Ryan</div>
<div className="user-item">Name: Nathan</div>
</div>
)
}
// Can be refactored to
function MyComponent() {
return (
<div className="browse-users">
<UserItem name={Brad}>
<UserItem name={Ryan}>
<UserItem name={Nathan}>
</div>
)
}하지만 상태(state)와 생명주기 메서드(lifecycle methods)를 재사용하는 것은 그렇게 명확하지 않습니다. 예를 들어, 클래스 기반 컴포넌트에서 상태와 생명주기 메서드를 가지고 상태를 관리하는 경우를 상상해봅시다. 이러한 메서드들은 클래스 내부에 정의되어 있기 때문에, 다른 클래스에서 이 메서드들을 그냥 "공유"할 수는 없습니다. 그리고 참고로, React에서는 우리가 직접 만든 베이스 컴포넌트를 상속받아서 사용하는 방식(OOP에서 흔히 쓰는 상속 기반 설계 방식) 은 권장하지 않습니다.
그래서, 리액트 커뮤니티에서는 두가지 패턴을 만들어냈습니다.
바로 고차 컴포넌트 (Higher Order Components) 와 Render Props 입니다.
따라서 리액트가 공식적인 방법을 가지고 있지 않아서 패턴들만을 가지고 있을때와 비교했을때, Hooks를 재사용가능한 상태와 기능들을 추상화하기 위한 “더 공식적인” 방법으로 생각할 수 있습니다.
시작하기 전에 이 두가지 중점사항을 이해하는 것이 중요합니다.
HoC
HoC는 두 컴포넌트를 부모와 자식으로 조합하는 방법입니다. 다음 파일을 확인해봅시다.
// File: MyComponent.js
import React from 'react'
function MyComponent({ props, from, HoC }) {
// ...
}
export default someHocFunction(MyComponent)
// ^^^^^ this function is the HoCMyComponent를 내보내고 있는 것으로 보입니다. 결국 이 파일은 MyComponent.js라고 불리게 되고 이것들을 임포트해서 사용할 때 import MyComponent from './MyComponent와 같이 작성하게 됩니다. 하지만 자세히 들여다보면, 실제로는 HoC 함수가 반환하는 것을 내보내고 있습니다. HoC는 컴포넌트를 부모 컴포넌트로 한번 감싸서 내보내고 있습니다. HoC는 자식으로서 당신의 컴포넌트와 함께 부모 컴포넌트를 돌려주고 있습니다. 우리가 <MyComponent /> 를 사용할 때 “MyComponent” 부분은 사실상 HoC의 부모 영역이고 실제 우리가 작성한 “MyComponent”가 아니라는 것을 인지하는것이 매우 중요합니다. 부모 요소가 “MyComponent”를 자식으로써 렌더링하고 있기때문에 가능한 일입니다. 계속 해보겠습니다.
이 HoC가 제공하는 부모 컴포넌트는 재사용가능한 상태와 기능을 가지고 있습니다. 부모 컴포넌트는 자식 컴포넌트가 prop을 통해 상태와 기능에 접근할 수 있도록 해줍니다.
재사용이라 함은, 아래와 같은 것을 할 수 있음을 의미합니다.
// MyMenu.js
export default withToggle(MyMenu)
// ActivateUser.js
export default withToggle(ActivateUser)메뉴를 위한 파일과 활동중인 사용자를 위한 파일 두가지를 가지고 있다고 생각해봅시다. 두가지 모두 무언가를 켜고 끌 수 있는 “toggle” 가능성이 필요합니다. 각각은 전환되는 상태와 기능을 위해 withToggle이라는 HoC를 사용합니다.
만약 이걸 잘 이해한다면, 이후엔 HoC의 단점들에 대해 얘기해볼 수 있을겁니다.
1. Prop Collisions
HoC의 첫번째, 가장 분명한 단점은 prop 충돌입니다. 만약 위에 했던 얘기들을 주의깊게 읽는다면, 우리가 아래와 같이 했을때 생기는 일을 눈치챌 수 있을 겁니다.
<MyComponent name="Brad" />우리는 사실상 name이라는 prop을 HoC에 의해 제공된 부모 컴포넌트에 전달하고 있습니다. 그 말인즉 우리는 MyComponent라고 작성한 컴포넌트에 name을 전달하고 있지 않다는 뜻입니다. 기술적으로, 해당 prop을 우리가 작성한 진짜 MyComponent에게 전달하는게 HoC의 역할입니다. 따라서 우리는 <MyComponent name="Brad" />가 직접적으로 MyComponent와 작동하고 있다고 느끼게 됩니다. 하지만 사실은 HoC로부터 온 부모 요소인 중간다리 컴포넌트가 존재하게 됩니다.
이것은 MyComponent에게 넘기고 싶었던 것과 HoC가 넘기고 싶어하는 것 사이에 prop 충돌이 일어날 수도 있다는 것을 의미합니다. 우리가 name을 넘기고 싶어할때 HoC또한 name이라 불리는 prop을 넘기고 싶어할 때 일어나는 일입니다. 자주 일어나진 않지만 충분히 일어날 수 있는 일이고, 우리의 코드에 버그를 일으킬 것입니다. 이 문제는 다음에 볼 Render Props와 Hook에는 일어나지 않습니다.
prop 충돌이 일어날때, 누군가는 가볍게 “글쎄, 이건 자주 일어나지도 않고 그냥 내 prop명을 충돌하지 않게 바꾸면 되는거잖아? 해결됐네” 라고 말할 수 있습니다. 하지만 때때로 우리는 여러개의 재사용 가능한 코드들을 컴포넌트에 합성할 필요가 있습니다.
export default someHoC(anotherHoc(coolHoc(MyComponent)))약간은 이상하게 보이지만, 동작합니다. 당신은 MyComponent를 동시에 하나 이상의 HoC로 감쌀 수 있습니다. 이것은 MyComponent가 각각이 상태와 기능적인 추상화를 전달하는 수많은 “부모”를 가질 것을 의미합니다. HoC가 우리에게 기능성을 전달하는 방법은 prop을 통하는 것이라는걸 기억하세요. 세가지의 HoC들을 보고 그 중 단 하나도 다른 것들과 같은 이름을 가진 prop을 가지지 않았다고 진짜로 말할 수 있나요? 아마 할 수 없을겁니다.
2. HoC를 두번 사용할 수 없다.
우리가 방금 본 것과 같이, 당신은 컴포넌트에 하나 이상의 HoC를 사용할 수 있습니다. 여러개의 추상화를 한번에 사용할 수 있는 것은 매우 유용한 일입니다. 하지만, 만약 같은 HoC를 두번 사용하고 싶다면 어떻게 될까요?
export default withToggle(withToggle(MyComponent))MyComponent가 전환되어야 할 두가지 요소를 가지고 있다면 어떻게 해야 할까요? 이제 우리는 큰 문제점 앞에 서있습니다. 왜냐하면 우리는 분명히 100% prop 충돌을 겪게 될 것이기 때문입니다.
3. 불명확성
전달받는 prop에 대해 말하자면, MyComponent가 받는 prop에 대해 살펴봅시다.
function MyComponent({ name, onClick, setValue, time, date, isActive, isRemoved }) {
// ...
}
export default someHoC(anotherHoc(coolHoc(MyComponent)))이 prop들 중 어떤게 어느 HoC에서 왔는지 말할 수 있습니까? 어떤 prop이 HoC에서 오지 않았고, <MyComponent name="Brad" />와 같이 스스로 추가되어야 하는지 추측할 수 있습니까?
4. 빌드 시간에 합성됨
HoC는 빌드 시간에 합성됩니다. 이것이 의미하는 바는 다음과 같습니다.
export default someHoC('Some Value', MyComponent)때때로 당신의 컴포넌트와 함께 HoC에 다른 값들을 전달하는건 괜찮습니다. 하지만 이러한 값들이 MyComponent가 요소로 전환되기 이전인 빌드시간에 전달되기 때문에, 우리는 MyComponent로 HoC에게 매개변수로 전달된 prop들을 사용할 수 없습니다. 설명해보겠습니다.
당신이 수많은 리액트 코드를 작성했고 많은 컴포넌트가 하나의 네트워크 요청을 만들어내야 하는 걸 봤고, JSX에서 사용하기 위해 네트워크 응답을 상태로 저장해야 할 필요가 있는 걸 봤다고 생각해봅시다. 당신은 영리하게 다음과 같이 만들기로 결정했습니다.
export default fetchData('/users', BrowseUsers)당신의 fetchData HoC는 첫번째 매개변수를 가져가 정보를 받기 위한 네트워크 요청을 /users로 받을 거고 그 결과를 받아 BrowseUsers에게 전달할 것입니다. 꽤나 멋진것 같은데요? 이제 당신은 “componentDidMount, fetch data, setState, use state in JSX” 라는 똑같은 패턴을 프로그래밍하지 않아도 됩니다.
처음에는 멋져 보였지만, 당신은 이후에 다음과 같은 컴포넌트를 가지고 있다는 것을 깨닫습니다.
<UserProfile uid={5} />당신은 다음과 같이 하고 싶습니다.
function UserProfile({ uid }) {
// ...
}
export default fetchData('/users/???', UserProfile)문제가 보이시나요? 당신이 5라는 값을 가진 uid prop에 접근하려는 순간은 사용자 정보를 가져오는 경로에 연결하기에는 너무나도 동적이라 나중에 무엇으로든 변할 수 있습니다. ???라고 표시해둔 경로에 어떻게 해야 5를 넣을 수 있을까요?
Render Props
Render Props는 HoC의 대안으로 고려되었습니다. Render Prop의 전제는 재사용 가능한 상태/기능을 가진 부모 요소를 만드는 것과 유사합니다. 하지만 이번에는 우리 컴포넌트를 감싸지 않습니다.
function MyComponent() {
return (
<div>
<h1>My Page</h1>
<Toggle render={(on, toggle) => {
return <button onClick={toggle}>The toggle is {on ? 'on' : 'off'}</button>
}} />
<footer>footer</footer>
</div>
)
}
export default MyComponent우리가 HoC처럼 토글을 재사용가능하게 했을때는, MyComponent 전체에 prop이 제공되었지만 JSX에서 해당 prop의 값을 필요로 할 가능성이 있었습니다. 대신에 위와 같이 Render Prop 패턴을 사용해봅시다. Toggle은 이제 그 자체로 토글 값을 관리하는 상태와 기능을 모두 가지고 있는 구성요소 입니다. 처음에는 조금 낯설어 보일 수 있습니다. render={fn} 이라는 prop에 함수를 전달하고 JSX를 반환합니다. 내부적으로 Toggle 컴포넌트는 render 함수를 다음과 같이 호출합니다.
class Toggle extends React.Component {
// ...
render() {
return this.props.render(this.state.on, this.state.toggle)
}
}Toggle이 실제로는 JSX를 가지고 있지 않고, 대신 this.props.render의 반환값을 사용하고 있다는 것에 주의하세요. 이 패턴을 사용하면 Toggle이 상태와 기능을 담당하지만 JSX는 외부인 MyComponent에서 제어하도록 합니다. 당신은 아마 처음엔 이렇게 생각할 수 있습니다.
“그냥 이렇게 하면 안되나요?”:
<Toggle>
<button></button>
<Toggle>이것도 분명히 button을 Toggle에게 전달하는 한 방법입니다. 하지만 이 상황에서 Toggle이 이것을 받을때는, 버튼은 우리가 쉽게 뭔가를 전달할 수 있는 게 아닌, this.props.children입니다. 우리는 다음과 같이 할 수 있습니다.
<Toggle>
{(on, toggle) => {
return <button onClick={toggle}>The toggle is {on ? 'on' : 'off'}</button>
}}
<Toggle>기술적으로, 우리는 함수를 children으로써 전달할 수 있습니다. 이것은 실제 prop을 전달해 render 함수를 호출하는 것과는 다르게 보일 수 있지만, 실제로는 정확히 같은 동작입니다. render 대신 children을 사용하는 것 또한 Render Props로 불립니다.
Render Prop은 HoC와 어떻게 비교되나요?
우리가 HoC를 사용할때 인식했던 대부분의 문제들은 우리가 Render Prop을 사용할때 없어집니다. prop들은 MyComponent에 보내지기 때문에 prop 충돌은 일어나지 않습니다.
function MyComponent() {
// ... code that has render props
}
export default MyComponent이 prop들은 추상화된 prop들(지금 상황에서는 Render Props)과 섞이지 않습니다. 우리는 <MyComponent name="brad" />와 같이 작성해 this.props.name이나 props.name으로 값에 접근할 수 있습니다. render prop이 name이라는 이름의 변수를 사용해야 한다면, 다음과 같이 작성할 수 있습니다.
function MyComponent({ name }) {
console.log(name)
return (
<div>
<SomeRenderPropsThing>
{({ name: otherName }) => {
// ...
}}
</SomeRenderPropsThing>
</div>
)
}
export default MyComponentHoC에서와는 달리 우리는 다른 별칭을 붙일 수 있다는 것에 주목하세요.
같은 Render Prop을 두번 사용하는 건 어떨까요? 쉬운 일입니다.
function MyComponent({ name }) {
console.log(name)
return (
<div>
<h1>My Page</h1>
<Toggle render={(on, toggle) => {
return <button onClick={toggle}>The toggle is {on ? 'on' : 'off'}</button>
}} />
<footer>footer</footer>
<Toggle render={(on, toggle) => {
return <button onClick={toggle}>The toggle is {on ? 'on' : 'off'}</button>
}} />
</div>
)
}
export default MyComponent그리고 Toggle을 통해 우리에게 전달되는 값은 서로 다른 두가지의 함수이기 때문에 충돌되지 않습니다. 또한 불명확성이 존재하지 않는 점에 주목하세요. 우리는 어디서 왔는지 모를 거대한 prop 뭉치를 가지고 있지 않습니다. MyComponent에 있는 모든 prop들은 우리로부터 전달됩니다. 그리고 Render Prop에서 우리에게 전달된 변수들은 MyComponent의 prop이 아니지만 해당 Render Prop 추상화에서 쉽게 식별될 수 있는 값입니다.
데이터 가져오기 추상화는 어떨까요?
데이터 가져오기를 HoC나 Render prop으로 만들어야 한다고 얘기하려는 게 아닙니다. 입력으로 일부 동적 데이터를 제공해야만 하는 추상화의 예를 설명하려고 할 뿐이라는 것을 명심하세요. 데이터 가져오기는 그저 쉬운 예시입니다.
이전에는 uid와 같은 prop을 받아서 추상화를 하는 코드에 전달해줬어야 했습니다.
<UserProfile uid={5} />
// the UserProfile code:
function UserProfile() {
}
export default fetchData('/how/do/I/get/the/prop/5/here??', UserProfile)Render prop을 사용한다면, 훨씬 쉬워집니다.
<UserProfile uid={5} />
// the UserProfile code:
function UserProfile({ uid }) {
return (
<div>
<FetchData url={`/users/${uid}`} render={results => {
return <div>User: results.name</div>
}}/>
</div>
)
}
export default UserProfileRender props는 HoC의 소개에서 말했던 모든 문제들을 해결합니다. 하지만, HoC에서는 절대 없었던 새로운 문제들을 (그 중 한 문제는 약간 어리석은 거일수도 있지만) 가지게 됩니다.
마침내, Hooks!!
Hooks는 우리가 단순히 논의한 두가지의 대안으로 사용될 패턴이 아니라는 것을 기억하세요. Hooks는 리액트 코드를 작성하는 완전히 새로운 방법이며 우리가 HoC와 Render Prop을 더이상 필요로 하지 않게 된 이유입니다. 왜냐하면 Hook은 문제들을 훨씬 좋은 방식으로 해결하기 때문입니다.
이것은 Hook이 어떻게 동작하는지에 대한 설명이 아닙니다. HoC와 Render prop과 어떻게 비교할 수 있는지 충분히 알고 있다고 가정하겠습니다.
HoC와 Render Prop에 존재하던 중심 “문제”는 추상화된 값이 컴포넌트에 “어떻게” 전달되는지에 대한 것입니다. 두가지 방법 모두 컴포넌트 바깥에서 prop이 왔기 때문에 우리는 prop 충돌과 불명확성, 또는 컴포넌트 모든 곳에서 접근 할 수 없도록 함수범위 내부에서만 받아야 되는 문제가 있었습니다. 우리가 custom hook을 사용할 때, 우리는 다음과 같은 걸 볼 수 있습니다.
function MyComponent({ uid }) {
const results = useFetch(`/users/${uid}`)
const { on: menuOn, toggle: toggleMenuOn } = useToggle()
const { on: userOn, toggle: toggleUserOn } = useToggle()
return (
<div>
// ...
</div>
)
}
export default MyComponentHook으로 해결되는 HoC의 문제점들
Hook으로 해결되는 Render Prop의 문제점들