React Beta 공식문서를 통해 학습한 cloneElement API에 대해 정리합니다.
cloneElement
cloneElement를 사용하면 다른 엘리먼트를 기반으로 새로운 React 엘리먼트를 만들 수 있습니다.
const clonedElement = cloneElement(element, props, ...children)
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage">Goodbye</Row>
Parameters
- element : 엘리먼트 인수는 유효한 React 엘리먼트여야 합니다.
- 예를 들어 <Something />과 같은 JSX 노드일 수 있습니다.
- 해당 노드는 createElement 혹은 다른 cloneElement의 호출 결과일 수 있습니다.
- props : props 인수는 객체이거나 null이어야 합니다.
- null을 전달하면 복제된 요소는 원래 element.props를 모두 유지합니다.
- null이 아닌 객체를 전달하면, 복사된 요소는 기존 요소의 element.props의 값보다 인수 props의 값을 "선호"합니다.
- 나머지 prop은 원래 element.props에서 채워집니다.
- props.key 또는 props.ref를 인자로 전달하면 원래 항목을 대체합니다.
- null을 전달하면 복제된 요소는 원래 element.props를 모두 유지합니다.
- (선택 사항): ..children: 0개 이상의 자식 노드입니다.
- React 엘리먼트, 문자열, 숫자, portals, 빈 노드(null, undefined, true 및 false) 및 React 노드 배열을 포함한 모든 React 노드가 될 수 있습니다.
- ...children 인수를 전달하지 않으면 원래 element.props.children이 보존됩니다.
Returns
- type : 리액트 요소의 타입(element.type)
- props : 리액트 요소의 props(element.props), 당신이 전달한 prop과 기존 prop의 얕은 병합 결과입니다.
- ref : 복사 대상 리액트 요소의 element.ref, props.ref로 덮어 쓸 수 있습니다.
- key : 복사 대상 리액트 요소의 element.key, props.key로 덮어 쓸 수 있습니다.
일반적으로 컴포넌트를 사용해 요소를 반환하거나 다른 요소의 자식으로 만듭니다.
요소의 속성을 읽을 수 있지만 요소가 생성된 후에는 불투명한 객체처럼 생각하는게 좋습니다.
- 내부를 들여다보기보단 렌더링에만 쓰는게 좋습니다.
주의사항(Caveats)
- 요소를 복제해도 원래 요소는 수정되지 않습니다.
- cloneElement(element, null, child1, child2, child3)와 같이 자식이 모두 정적으로 알려진 경우에만 cloneElement에 여러 인수로 자식을 전달해야 합니다.
- 자식 갯수가 동적이면 전체 배열을 세 번째 인수인 cloneElement(element, null, listItems)로 전달합니다
- 이렇게 하면 React가 동적 배열의 누락된 키에 대해 경고합니다.(warn you about missing keys)
- 정적 목록의 경우 재정렬되지 않기 때문에 key가 필요하지 않습니다.
- cloneElement를 사용하면 데이터 흐름을 추적하기가 더 어려워지므로 다른 대안(alternatives)을 사용해 보세요
사용 사례
요소의 props 덮어쓰기
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />, // prop을 덮어 쓸 요소
{ isHighlighted: true } // 덮어 쓸 prop
);
next 버튼을 클릭해 선택한 행을 변경하는 리스트 컴포넌트를 생각해봅시다.
List 컴포넌트는 선택한 행을 다르게 렌더링해야 하므로 수신한 모든 <Row> 자식을 복사합니다.
isHighlighted: true 또는 isHighlighted: false prop을 추가합니다.
export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
대안
cloneElement와 Children API는 데이터 흐름 추적을 어렵게 만들기 때문에, 대안을 사용하는 것이 좋습니다.
render props로 데이터 전달하기
cloneElement를 사용하는 대신 renderItem과 같은 render prop을 사용할 수 있습니다.
List는 renderItem을 prop으로 받습니다.
List는 모든 item 배열의 데이터에 대해 renderItem을 호출하고 isHighlighted를 인수로 전달합니다.
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return renderItem(item, isHighlighted);
})}
renderItem prop은 컴포넌트를 렌더링하는 방법(how)에 관한 prop이기 때문에 “render prop”이라고 합니다.
예를 들어 주어진 isHighlighted 값으로 <Row>를 렌더링하는 renderItem 구현을 전달할 수 있습니다.
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>
중첩할 경우 코드 보기가 힘들어져서, 컴파운드 컴포넌트 패턴을 선호합니다.
context로 데이터 전달하기
cloneElement 사용의 다른 대안은 데이터를 context로 넘기는 것입니다.(pass data through context.)
export const HighlightContext = createContext(false);
주 : 컨텍스트가 List 컴포넌트에 하나가 아니라 여러개가 됩니다.
그리고 그 컨텍스트에 값이 selectedIndex가 있을 수도 있고 없을 수도 있습니다.
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}
이 접근 방식을 사용하면 Row는 isHighlighted prop이 전혀 필요 없습니다.
대신 컨텍스트에서 해당 값을 가져옵니다.
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>
대신 List와 Row는 컨텍스트를 통해 강조 표시 논리를 조정합니다.(컨텍스트를 통핸 로직 캡슐화)
해당 패턴은 컴포넌트 내에 상태 관리 로직을 캡슐화하고 싶을 때 좋습니다.
커스텀 훅으로 로직 추출하기
"비시각적" 로직(ui와 무관한)을 자신의 Hook으로 추출하고 Hook에서 반환된 정보를 사용하여 무엇을 렌더링할지 결정하는 것입니다.
예를 들어 다음과 같이 useList 커스텀 훅을 작성할 수 있습니다.
import { useState } from 'react';
export default function useList(items) {
const [selectedIndex, setSelectedIndex] = useState(0);
function onNext() {
setSelectedIndex(i =>
(i + 1) % items.length
);
}
const selected = items[selectedIndex];
return [selected, onNext];
}
export default function App() {
const [selected, onNext] = useList(products);
return (
<div className="List">
{products.map(product =>
<Row
key={product.id}
title={product.title}
isHighlighted={selected === product}
/>
)}
<hr />
<button onClick={onNext}>
Next
</button>
</div>
);
}
데이터 흐름이 명시적이며,
상태는 모든 컴포넌트에서 사용할 수 있는 useList 커스텀 훅 내부에 있습니다.
해당 패턴은 여러 컴포넌트에서 특정 반복되는 로직을 재사용할 때 사용하면 좋습니다.
'FrontEnd' 카테고리의 다른 글
styled-components의 동작 원리와 주의사항 (0) | 2023.02.17 |
---|---|
빅테크 프론트엔드 기술 인터뷰 : JS 편 (0) | 2023.02.17 |
리액트 디자인 패턴(React design pattern) : Compound Component Pattern(컴파운드 컴포넌트패턴)과 Uncontrolled Component Pattern(유상태 컴포넌트 패턴) (0) | 2023.02.15 |
[React] 리액트 Children, React Children API의 모든 것 (0) | 2023.02.15 |
Stitches와 Radix를 이용해 디자인 시스템 만들기 (0) | 2023.02.14 |