React의 Render Props 패턴을 활용한 컴포넌트 디자인
React 컴포넌트 디자인 패턴 중 하나인 Render Props 패턴에 대해 알아보겠습니다.
공통 부분을 자식 컴포넌트에서 횡단적으로 사용하는 기법으로 소개되고 있지만, 익숙하지 않은 분들이 많을 것 같습니다.
헤드리스 UI 라이브러리인 React Aria를 접하면서 Render Props가 활용되고 있는 것을 보고, 이를 기반으로 정리하였습니다.
Render Props 란
Render Props로 구현된 컴포넌트는 React Element를 반환하는 함수를 prop으로 가집니다.
const ComponentWithRenderProp = ({ render }) => {
return render();
};
// 호출 부분
<ComponentWithRenderProp render={() => <div>Render Prop</div>} />
실제로는 단순한 함수이므로, 인자를 전달하거나, prop을 새로 생성하는 것이 아니라, children을 사용할 수도 있습니다.
export const ComponentWithRenderProp = ({ children }) => {
const id = useId();
return children(id);
};
// 호출 부분
<ComponentWithRenderProp>
{(id) => <div>{`id: ${id}`}</div>}
</ComponentWithRenderProp>
React Aria에서의 Render Props
다음으로 버튼을 예로 들어, React Aria의 코드를 살펴보겠습니다.
React Aria는 헤드리스 UI이므로, 버튼을 만드는 데 필요한 "행동"을 제공해 줍니다.
버튼이라고 하면, 호버나 포커스가 걸리는 경우 "모양"을 바꾸는 것을 생각할 수 있는데, Render Props는 이러한 상태에 접근하는 수단으로 제공되고 있으며, 모양은 상태를 사용하여 자유롭게 변경할 수 있습니다.
React Aria 컴포넌트는 기본적으로 스타일을 포함하지 않습니다.
TypeScript의 타입 정의를 보면 다음과 같이 되어 있으며, ButtonRenderProps로 정의된 상태를 Render prop인 children의 인자로 받을 수 있음을 알 수 있습니다.
// 버튼의 Props: `RenderProps<ButtonRenderProps>`를 상속하고 있음
export interface ButtonProps extends Omit<AriaButtonProps, 'children' | 'href' | 'target' | 'rel' | 'elementType'>, HoverEvents, SlotProps, RenderProps<ButtonRenderProps> {
form?: string;
// ...
}
// Render Props: `children`의 타입 정의를 덮어써서, 제네릭 타입 (T)를 받고, ReactNode를 반환하는 함수로 함
interface RenderProps<T> extends StyleRenderProps<T> {
children?: ReactNode | ((values: T & {
defaultChildren: ReactNode | undefined;
}) => ReactNode);
}
// 버튼의 Render Props: 버튼의 상태가 정의되어 있음
export interface ButtonRenderProps {
isHovered: boolean;
isPressed: boolean;
// ...
}
버튼 사용해 보기
타입 정의에서 사용법을 읽어낼 수 있으므로, 호버 시 표시 텍스트가 바뀌는 버튼을 만들어 보겠습니다.
HoverButton.tsx
import type { ComponentPropsWithRef } from "react";
import { Button } from "react-aria-components";
type Props = ComponentPropsWithRef<typeof Button>;
export const HoverButton = (props: Props) => (
<Button {...props}>
{({ isHovered }) => (isHovered ? "호버 중 🐁" : "호버해 주세요")}
</Button>
);
범용적인 컴포넌트를 제공하는 경우, 유스케이스가 무한정 존재하게 됩니다.
따라서, 공통 부분이 되는 행동에는 Render Props를 통해 접근할 수 있는 것은 좋은 설계라고 생각했습니다.
체크박스 그룹 만들어 보기
마지막으로 Render Props를 사용하여, 체크박스 그룹을 만들어 보겠습니다.
CheckboxGroup.tsx
import { type ReactNode, useState } from "react";
type CheckboxGroupRenderProps = {
selectedValues: string[];
setValue: (value: string[]) => void;
};
type CheckboxGroupProps = {
children: ReactNode | ((values: CheckboxGroupRenderProps) => ReactNode);
};
export const CheckboxGroup = ({ children }: CheckboxGroupProps) => {
const [selectedValues, setSelectedValues] = useState<string[]>([]);
// `children`이 Render Prop일 경우, 상태와 세터 함수를 인자로 전달
if (typeof children === "function") {
return children({ selectedValues, setValue: setSelectedValues });
}
return children;
};
type CheckboxProps = CheckboxGroupRenderProps & {
label: string;
value: string;
};
export const Checkbox = ({
selectedValues,
setValue,
label,
value,
}: CheckboxProps) => {
const handleClick = () => {
const isSelected = selectedValues.includes(value);
if (isSelected) {
setValue(selectedValues.filter((v) => v !== value));
} else {
setValue([...selectedValues, value]);
}
};
return (
<label>
<span>{label}</span>
<input
type="checkbox"
checked={selectedValues.includes(value)}
onClick={handleClick}
/>
</label>
);
};
Home.tsx
import { Checkbox, CheckboxGroup } from "@/components/checkbox-group";
export default function Home() {
return (
<CheckboxGroup>
{({ selectedValues, setValue }) => (
<>
<Checkbox
label="🍎"
value="apple"
selectedValues={selectedValues}
setValue={setValue}
/>
<Checkbox
label="🍌"
value="banana"
selectedValues={selectedValues}
setValue={setValue}
/>
<Checkbox
label="🍇"
value="grape"
selectedValues={selectedValues}
setValue={setValue}
/>
<div>
<div>선택된 값:</div>
<ul>
{selectedValues.map((value) => (
<li key={value}>{value}</li>
))}
</ul>
</div>
</>
)}
</CheckboxGroup>
);
}
결론
이번에 Render Props 패턴을 학습했습니다.
React Aria뿐만 아니라, 라이브러리의 API로 자주 보이는 것 같으므로, 메커니즘을 이해하고 활용할 수 있도록 하고 싶습니다.
'Javascript' 카테고리의 다른 글
자바스크립트 개발자를 위한 Promise 완벽 가이드: 콜백 지옥 탈출하기 (1) | 2024.12.28 |
---|---|
React 19 주요 기능 가볍게 살펴보기 (1) | 2024.12.21 |
JavaScript로 Cookie에 데이터 저장하는 방법 알아보기 (0) | 2024.11.24 |
JavaScript 오브젝트 메서드 완벽 이해하기: Object.entries, Object.fromEntries (0) | 2024.11.24 |
Next.js 오류 처리에서 흔히 겪는 문제와 해결 방법 (0) | 2024.11.24 |