React에서는 HOC, Custom Hook 등 컴포넌트와 데이터, 로직을 재사용하기 위한 여러가지 기법이 있다. 이번에는 그 중에서 render props 패턴이란걸 알아보자.
Render Props 패턴이란?
아주 간단하게, Render Props는 컴포넌트의 prop으로 JSX를 반환하는 함수를 전달하여, 무엇을 렌더링할지를 부모(외부)에게 위임하는 패턴이다. 간단한 예시로 보면 다음과 같다. (참고로 props이름이 꼭 render일 필요는 없다.)
// (1) 껍데기를 제공하는 컴포넌트
const Box = ({ render }) => {
return (
<div style={{ border: '2px solid blue', padding: '20px', borderRadius: '8px' }}>
{render()}
</div>
);
};
// (2) 사용부: "나는 텍스트를 넣을래"
<Box render={() => <span>간단한 텍스트입니다.</span>} />
// (3) 사용부: "나는 이미지를 넣을래"
<Box render={() => <img src="logo.png" alt="로고" />} />이 처럼 함수를 JSX를 반환하는 props로 받아 호출하는 패턴이다. 너무 간단해서 아직까지는 이 패턴이 왜 유용한지 어떻게 쓸 수 있는지 잘 모르겠다.
데이터를 전달하는 Render Props
조금 더 발전 시켜보자. 예를 들어 자식 컴포넌트에서 특정 데이터를 관리하고 있지만, 부모에 따라서 그 데이터를 다르게 보여줄 필요가 있을 수 있다.
// (1) 데이터를 가지고 있는 컴포넌트
const UserProvider = ({ render }) => {
const user = { name: "Sanghyeon", role: "Developer" };
// 가진 데이터를 render 함수에 실어서 보냄
return <div>{render(user)}</div>;
};
// (2) 사용부 A: 이름만 크게 보고 싶을 때
<UserProvider render={(user) => <h1>{user.name}</h1>} />
// (3) 사용부 B: 역할과 이름을 같이 보고 싶을 때
<UserProvider render={(user) => <p>{user.name} ({user.role})</p>} />이러한 방식으로 우리는 컴포넌트가 가진 데이터를 외부(부모)에서 자유롭게 다루며 렌더링할 수 있다.
children prop 활용하기
위 예시에서는 render 라는 이름의 prop을 사용했지만, children prop을 활용하는 것이 더 일반적이며, 이 방식을 사용하면 일반적인 컴포넌트처럼 자식을 감싸는 형태가 되어 가독성이 좋아진다.
// (1) render prop 대신 children을 함수로 실행
const UserProvider = ({ children }) => {
const user = { name: "Sanghyeon", role: "Developer" };
return <div>{children(user)}</div>;
};
// (2) 사용부: 훨씬 직관적인 구조가 된다.
<UserProvider>
{(user) => (
<div>
<h1>{user.name}</h1>
<p>{user.role}</p>
</div>
)}
</UserProvider>Hooks로 대체
하지만 지금의 코드도, hook으로 충분히 대체할 수 있으며 단순히 데이터만 다루게 된다면 hook을 사용하는게 더 깔끔하고 단순할 수 있다.
// hook으로 전환한 코드
const useUser = () => {
const user = { name: "Sanghyeon", role: "Developer" };
return user;
};
// (1) 사용부 A: 이름만 크게
function LargeName() {
const user = useUser();
return <h1>{user.name}</h1>;
}
// (2) 사용부 B: 역할과 이름
function NameAndRole() {
const user = useUser();
return <p>{user.name} ({user.role})</p>;
}요즘의 리액트 개발에서는 많은 Render Props 패턴이 Hook으로 대체되었다. 하지만 Hook이 해결하기 어려운 영역이 있다. 바로 "로직에 특정 DOM 구조가 결합되어야 할 때" 이다.
제대로 활용해보기
무한스크롤을 해보자. 무한스크롤을 구현하기 위한 다양한 방법이 있겠지만, IntersectionObserver와 스크롤의 끝을 감지하는 더미 컴포넌트를 이용한 방법으로 구현한다고 해보자.
function PostListContainer() {
const { items, fetchNextPage, hasMore, isLoading } = useInfiniteScrollHook();
const observerRef = useRef(null); // 사용자가 직접 관리
return (
<div>
<PostList items={items} />
// (1) 불편함: 데이터(isLoading)에 따른 UI 처리를 여기서 매번 직접 해야 함
{isLoading && <MyCustomSpinner />}
// (2) 불편함: 로직을 위한 '더미 div'를 잊지 말고 직접 넣어야 함
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}Hook은 로직만 제공할 뿐, ref와 DOM연결은 개발자가 직접 다시 해야한다. 이러한 로직이 반복된다면, 꽤 불편한 작업이 될 것이다.
Render Props 방식으로 수정
// (1) Render Props 컴포넌트
function InfiniteScroll({ onLoadMore, hasMore, children }) {
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef(null);
useEffect(() => {
if (!hasMore || isLoading) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsLoading(true);
onLoadMore().finally(() => setIsLoading(false));
}
});
if (observerRef.current) observer.observe(observerRef.current);
return () => observer.disconnect();
}, [hasMore, onLoadMore, isLoading]);
return (
<div>
// 자식 함수에 현재 로딩 상태를 넘겨준다
{children(isLoading)}
// 필수 더미 요소는 컴포넌트가 책임진다
{hasMore && <div ref={observerRef} style={{ height: '20px' }} />}
</div>
);
}
// (2) 사용부: 내부 상태인 isLoading을 받아서 UI를 결정한다
<InfiniteScroll hasMore={hasMore} onLoadMore={fetchNextPage}>
{(isLoading) => (
<>
<PostList items={items} />
// 로딩 바를 리스트 바로 밑에 둘지, 우측 상단에 작게 띄울지 부모가 결정한다
{isLoading && <MyCustomSpinner />}
</>
)}
</InfiniteScroll>Render Props 패턴을 이용하면 로직과 필수 DOM 구조를 한 세트로 제공할 수 있다. 사용자는 observerRef를 신경쓸 필요가 없어지고, isLoading 을 children prop으로 넘겨서 외부에서 어떻게 표시할지 컨트롤할 수 있게 되었다.
장점과 단점
장점
- 로직과 구조의 완전한 캡슐화:
InfiniteScroll예제처럼 로직이 돌아가기 위해 특정 DOM 요소(더미 div 등)가 반드시 필요할 때, 이를 컴포넌트 내부에 숨겨서 재사용성을 극대화할 수 있다. - 제어권의 위임: 데이터는 컴포넌트가 관리하지만, "어떻게 보여줄지"는 사용하는 부모가 100% 결정한다. 덕분에 매우 유연한 UI 구성이 가능해진다.
단점
- Render Prop Hell (중첩 문제): 여러 개의 Render Props를 중첩해서 사용하게 될 경우, 코드의 깊이가 깊어져 가독성이 급격히 떨어질 수 있다.
- Hook에 비해 장황한 코드: 단순한 로직 공유라면 Hook을 사용하는 것보다 작성해야 할 코드가 많아지고 구조가 복잡해 보일 수 있다.
마무리
단순히 데이터나 로직을 재활용하는 용으로는 Hook으로도 충분하다. 하지만 로직과 DOM 구조까지 캡슐화 하고 싶다면 Render Props 패턴을 한 번 사용해보자.
그럼에도 아직 어떻게 활용할지 모르겠다면 Render Props 패턴을 활용한 라이브러리 모음이 있으니 여기서 영감을 받아보자!
참조