
children에 props 를 주입할 수 있나 ?최근에 children 을 넘겨받는 재사용 가능한 컴포넌트를 만든적이 있습니다.
흔히 React 개발 상황에서 많이 마주하는 상황일 것 같아요.

예를 들어, 이런 모습입니다. 익숙하죠 ?
const RenewableComponent = ({ children }: { children: ReactNode }) => {
// 공통으로 사용되는 state, logic
return (
<>
{/* 공통적인 UI */}
{children}
</>
);
};
컴포넌트를 사용하는 곳은 이렇게 될 수 있겠죠.
export const TextPage = () => {
return (
<RenewableComponent>
<div>내부 컨텐츠</div>
</RenewableComponent>
);
};
기가 막힙니다. 통일성있는 UI에 원하는 컴포넌트를 삽입하다니, children 기능은 멋집니다.
그러나, RenewableComponent 컴포넌트에서 선언한 state 를 children으로 넘기고 싶어질 수 있지 않을까요 ??
그렇다면 예시를 좀 바꿔야겠습니다.
재사용할 컴포넌트인 RenewableComponent를 NameInputComponent 로 이름을 바꾸고, 기능도 추가했습니다. 사용자의 이름을 받는 컴포넌트네요.
const NameInputComponent = ({ children }: { children: JSX.Element }) => {
const [name, setName] = useState('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
{children}
</div>
);
};
사용할 때는 아래와 같습니다. 재사용 컴포넌트에서 입력한 name 을 사용자에게 표출하고 싶는데 값을 부모 컴포넌트로 받아올 방법이 없습니다.
export const MyPage = () => {
return (
<NameInputComponent>
<div>{name} 안녕하세요 !</div> // name을 NameInputComponent에서 받아오고 싶다.
</NameInputComponent>
);
};
children에 props를 넘긴다면 참 좋을텐데요.
const NameInputComponent = ({ children }: { children: JSX.Element }) => {
const [name, setName] = useState('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
{children} // 이곳에 name을 prop로 넘기면 좋을텐데,,,
</div>
);
};
어떻게 해야할까요 ?
Render Props Pattern으로 구현해보도록 하겠습니다.
재사용할 컴포넌트를 먼저 고치겠습니다.
props로 children 을 받지 않고, render 라는 이름으로 함수 하나를 받겠습니다. 이 render 함수에 name을 넘겨주면 다른 컴포넌트에서도 사용할 수 있을겁니다.
type TProps = {
render: ({ name }: { name: string }) => JSX.Element;
};
const NameInputComponent = ({ render }: TProps) => {
const [name, setName] = useState('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
{render({ name })}
</div>
);
};
사용할 컴포넌트에서는 아래처럼 선언합니다.
render 라는 props에 JSX 를 반환하는 함수를 선언해주면, render 함수의 매개변수를 JSX에 사용할 수 있습니다 !
export const MyPage = () => {
return (
<NameInputComponent
render={({ name }) => <div>{name} 님 안녕하세요 !</div>}
/>
);
};
훌륭하네요. 제가 원하는 재사용 컴포넌트에서 사용되는 값을 받아서 사용할 수 있게 되었습니다 !
재사용 컴포넌트를 이렇게 수정할 수 있습니다.
render 함수를 JSX 이름으로 재할당 받아서 JSX 컴포넌트 처럼 사용하는 것 입니다. (이건 취향 차이라고 생각합니다.)
type TProps = {
render: ({ name }: { name: string }) => JSX.Element;
};
// RenderItem 으로 재할당
const NameInputComponent = ({ render: RenderItem }: TProps) => {
const [name, setName] = useState('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* JSX 로 사용 */}
<RenderItem name={name} />
</div>
);
};
수정하고 보니 느낀게, 많이 본 패턴 같습니다.
react hook form을 사용해보신 분들은 <Controller /> 컴포넌트와 비슷한듯 합니다.
아래의 코드가 공식 문서에 있는 Controller 사용 예제입니다.
Controller 의 props로 render 함수가 있고, 그 함수의 매개변수를 임의의 컴포넌트에서 사용할 수 있는 구조네요 !
위에서 저희가 구현한 코드와 같습니다 !
function App() {
const { control } = useForm<FormValues>()
return (
<form>
<Controller
control={control}
name="ReactDatepicker"
render={({ field: { onChange, onBlur, value, ref } }) => (
<ReactDatePicker
onChange={onChange}
onBlur={onBlur}
selected={value}
/>
)}
/>
<input type="submit" />
</form>
)
react hook form 깃허브 controller 구현 코드 를 살펴보니, 역시 render props 패턴을 사용하고 있군요 !
const Controller = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
props: ControllerProps<TFieldValues, TName>,
) => props.render(useController<TFieldValues, TName>(props));
export { Controller };
조금 다른 건 render 함수에 hook 을 반환하는군요. 그래서 Contoller 와 useController 는 같은 기능을 구현하나봅니다. 역시 아는만큼 보인다는 사실을 또 느낍니다.
이렇게 유용한 패턴하나를 익힌 것 같아 기분이 좋습니다.
어찌됐든 React는 js, ts 의 도구일 뿐이고, 컴포넌트를 구현하는 방식도 다양합니다. 상황에 맞는 적절한 방식으로 개발하면 좋을 것 같습니다.