cloneElement
cloneElement๋ฅผ ์ฌ์ฉํ๋ฉด element๋ฅผ ๊ธฐ์ค์ผ๋ก ์๋ก์ด React ์๋ฆฌ๋จผํธ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
const clonedElement = cloneElement(element, props, ...children)๋ ํผ๋ฐ์ค
cloneElement(element, props, ...children)
์๋ก์ด React ์๋ฆฌ๋จผํธ๋ฅผ ๋ง๋ค๊ธฐ ์ํด element๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๊ณ , props์ children์ ๋ค๋ฅด๊ฒ ํ์ฌ cloneElement๋ฅผ ํธ์ถํ์ธ์.
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>์๋์์ ๋ ๋ง์ ์์ ๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
๋งค๊ฐ๋ณ์
-
element:element์ธ์๋ ์ ํจํ React ์๋ฆฌ๋จผํธ์ฌ์ผ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด,<Something />๊ณผ ๊ฐ์ JSX ๋ ธ๋,createElement๋ก ํธ์ถํด ์ป์ ๊ฒฐ๊ณผ๋ฌผ ๋๋ ๋ค๋ฅธcloneElement๋ก ํธ์ถํด ์ป์ ๊ฒฐ๊ณผ๋ฌผ์ด ๋ ์ ์์ต๋๋ค. -
props:props์ธ์๋ ๊ฐ์ฒด ๋๋null์ด์ด์ผ ํฉ๋๋ค.null์ ์ ๋ฌํ๋ฉด ๋ณต์ ๋ ์๋ฆฌ๋จผํธ๋ ์๋ณธelement.props๋ฅผ ๋ชจ๋ ์ ์งํฉ๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉดprops๊ฐ์ฒด์ ๊ฐ prop์ ๋ํด ๋ฐํ๋ ์๋ฆฌ๋จผํธ๋element.props์ ๊ฐ๋ณด๋คprops์ ๊ฐ์ โ์ฐ์ โํฉ๋๋ค. ๋๋จธ์งprops๋ ์๋ณธelement.props์์ ์ฑ์์ง๋๋ค.props.key๋๋props.ref๋ฅผ ์ ๋ฌํ๋ฉด ์๋ณธ์ ๊ฒ์ ๋์ฒดํฉ๋๋ค. -
(์ ํ์ฌํญ)
...children: 0๊ฐ ์ด์์ ์์ ๋ ธ๋๊ฐ ํ์ํฉ๋๋ค. React ์๋ฆฌ๋จผํธ, ๋ฌธ์์ด, ์ซ์, portals, ๋น ๋ ธ๋ (null,undefined,true,false) ๋ฐ React ๋ ธ๋ ๋ฐฐ์ด์ ํฌํจํ ๋ชจ๋ React ๋ ธ๋๊ฐ ํด๋นํ ์ ์์ต๋๋ค....children์ธ์๋ฅผ ์ ๋ฌํ์ง ์์ผ๋ฉด ์๋ณธelement.props.children์ด ์ ์ง๋ฉ๋๋ค.
๋ฐํ๊ฐ
cloneElement๋ ๋ค์๊ณผ ๊ฐ์ ํ๋กํผํฐ๋ฅผ ๊ฐ์ง React ์๋ฆฌ๋จผํธ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
type:element.type๊ณผ ๋์ผํฉ๋๋ค.props:element.props์ ์ ๋ฌํprops๋ฅผ ์๊ฒ ๋ณํฉํ ๊ฒฐ๊ณผ์ ๋๋ค.ref:props.ref์ ์ํด ์ฌ์ ์๋์ง ์์ ๊ฒฝ์ฐ ์๋ณธelement.ref์ ๋๋ค.key:props.key์ ์ํด ์ฌ์ ์๋์ง ์์ ๊ฒฝ์ฐ ์๋ณธelement.key์ ๋๋ค.
์ผ๋ฐ์ ์ผ๋ก ์ปดํฌ๋ํธ์์ ์๋ฆฌ๋จผํธ๋ฅผ ๋ฐํํ๊ฑฐ๋ ๋ค๋ฅธ ์๋ฆฌ๋จผํธ์ ์์์ผ๋ก ๋ง๋ญ๋๋ค. ์๋ฆฌ๋จผํธ์ ํ๋กํผํฐ๋ฅผ ์ฝ์ ์ ์์ง๋ง, ์์ฑ๋ ํ์๋ ๋ชจ๋ ์๋ฆฌ๋จผํธ์ ํ๋กํผํฐ๋ฅผ ์ฝ์ ์ ์๋ ๊ฒ์ฒ๋ผ ์ทจ๊ธํ๊ณ ๋ ๋๋งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์ฃผ์
-
์๋ฆฌ๋จผํธ๋ฅผ ๋ณต์ ํด๋ ์๋ณธ ์๋ฆฌ๋จผํธ๋ ์์ ๋์ง ์์ต๋๋ค.
-
์์์ด ๋ชจ๋ ์ ์ ์ธ ๊ฒฝ์ฐ์๋ง
cloneElement(element, null, child1, child2, child3)์ ๊ฐ์ด ์์์ ์ฌ๋ฌ ๊ฐ์ ์ธ์๋ก ์ ๋ฌํด์ผ ํฉ๋๋ค. ์์์ด ๋์ ์ผ๋ก ์์ฑ๋์๋ค๋ฉดcloneElement(element, null, listItems)์ ๊ฐ์ด ์ ์ฒด ๋ฐฐ์ด์ ์ธ ๋ฒ์งธ ์ธ์๋ก ์ ๋ฌํด์ผ ํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด React๊ฐ ๋ชจ๋ ๋์ ๋ฆฌ์คํธ์ ๋ํด key๊ฐ ๋๋ฝ๋์๋ค๋ ๊ฒฝ๊ณ ๋ฅผ ๋ณด์ฌ์ค๋๋ค. ์ ์ ๋ฆฌ์คํธ์ ๊ฒฝ์ฐ๋ ์์๊ฐ ๋ณ๊ฒฝ๋์ง ์์ผ๋ฏ๋ก ์ด ์์ ์ ํ์ํ์ง ์์ต๋๋ค. -
cloneElement๋ ๋ฐ์ดํฐ ํ๋ฆ์ ์ถ์ ํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ๋ค์ ๋์์ ์ฌ์ฉํด ๋ณด์ธ์.
์ฌ์ฉ๋ฒ
์๋ฆฌ๋จผํธ์ props ์ฌ์ ์ํ๊ธฐ
์ผ๋ถ React ์๋ฆฌ๋จผํธ์ props๋ฅผ ์ฌ์ ์ํ๋ ค๋ฉด ์ฌ์ ์ํ๋ ค๋ props๋ฅผ cloneElement์ ์ ๋ฌํ์ธ์.
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);clonedElement์ ๊ฒฐ๊ณผ๋ <Row title="Cabbage" isHighlighted={true} />๊ฐ ๋ฉ๋๋ค.
์ด๋ค ๊ฒฝ์ฐ์ ์ ์ฉํ์ง ์์ ๋ฅผ ํตํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
children์ ์ ํํ ์ ์๋ ํ ๋ชฉ๋ก์ผ๋ก ๋ ๋๋งํ๊ณ , ์ ํ๋ ํ์ ๋ณ๊ฒฝํ๋ โ๋ค์โ ๋ฒํผ์ด ์๋ List ์ปดํฌ๋ํธ๋ฅผ ์์ํด ๋ณด์ธ์. 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์์ ์ ๋ฌ๋ฐ์ ์๋ณธ JSX๊ฐ ์๋ค๊ณ ๊ฐ์ ํฉ์๋ค.
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>์์ ์์๋ฅผ ๋ณต์ ํจ์ผ๋ก์จ List๋ ๋ชจ๋ Row ์์ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค. ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>โ๋ค์โ ๋ฒํผ์ ๋๋ฅด๋ฉด List์ state๊ฐ ์
๋ฐ์ดํธ๋๊ณ ๋ค๋ฅธ ํ์ด ํ์ด๋ผ์ดํธ ํ์๊ฐ ๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
import { Children, cloneElement, useState } from 'react'; export default function List({ children }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {Children.map(children, (child, index) => cloneElement(child, { isHighlighted: index === selectedIndex }) )} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % Children.count(children) ); }}> ๋ค์ </button> </div> ); }
์์ฝํ์๋ฉด, List๋ ์ ๋ฌ๋ฐ์ <Row /> ์๋ฆฌ๋จผํธ๋ฅผ ๋ณต์ ํ๊ณ ์ถ๊ฐ๋ก ๋ค์ด์ค๋ prop ๋ํ ์ถ๊ฐํฉ๋๋ค.
๋์
render prop์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๊ธฐ
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์ ๋ ๋๋ง ๋ฐฉ๋ฒ์ ์ง์ ํ๋ prop์ด๊ธฐ ๋๋ฌธ์ โrender propโ์ด๋ผ๊ณ ๋ถ๋ฆฝ๋๋ค. ์๋ฅผ ๋ค์ด, ์ฃผ์ด์ง isHighlighted ๊ฐ์ผ๋ก <Row>๋ฅผ ๋ ๋๋งํ๋ renderItem์ ์ ๋ฌํ ์ ์์ต๋๋ค.
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>์ต์ข
์ ์ผ๋ก cloneElement์ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋ฉ๋๋ค.
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>ํ์ง๋ง isHighlighted ๊ฐ์ ์ถ์ฒ๋ฅผ ๋ช
ํํ๊ฒ ์ถ์ ํ ์ ์์ต๋๋ค.
import { useState } from 'react'; 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); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> ๋ค์ </button> </div> ); }
์ด๋ฌํ ํจํด์ ๋ ๋ช
์์ ์ด๊ธฐ ๋๋ฌธ์ cloneElement ๋ณด๋ค ์ ํธ๋ฉ๋๋ค.
Context๋ฅผ ํตํด ๋ฐ์ดํฐ ์ ๋ฌํ๊ธฐ
cloneElement์ ๋ ๋ค๋ฅธ ๋์์ผ๋ก๋ Context๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฒ์
๋๋ค.
์๋ฅผ ๋ค์ด, createContext๋ฅผ ํธ์ถํ์ฌ HighlightContext๋ฅผ ์ ์ํ ์ ์์ต๋๋ค.
export const HighlightContext = createContext(false);List ์ปดํฌ๋ํธ๋ ๋ ๋๋งํ๋ ๋ชจ๋ item์ HighlightContext.Provider๋ก ๊ฐ์ ์ ์์ต๋๋ค.
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์ ๋ฐ์ ํ์๊ฐ ์์ด์ง๋๋ค. ๋์ context๋ฅผ ์ฝ์ต๋๋ค.
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...์ด์ ๋ฐ๋ผ isHighlighted๋ฅผ <Row>๋ก ์ ๋ฌํ๋ ๊ฒ์ ๋ํด ํธ์ถ๋ ์ปดํฌ๋ํธ๊ฐ ์๊ฑฐ๋ ๊ฑฑ์ ํ์ง ์์๋ ๋ฉ๋๋ค.
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>๋์ ์ List์ Row๋ context๋ฅผ ํตํด ํ์ด๋ผ์ดํ
๋ก์ง์ ์กฐ์ ํฉ๋๋ค.
import { useState } from 'react'; import { HighlightContext } from './HighlightContext.js'; 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> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> ๋ค์ </button> </div> ); }
context๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ ๋ํ์ฌ ์์ธํ ์์๋ณด์ธ์.
Custom Hook์ผ๋ก ๋ก์ง ์ถ์ถํ๊ธฐ
๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์์ผ๋ก๋ ์์ฒด hook์ ํตํด โ๋น์๊ฐ์ ์ธโ ๋ก์ง์ ์ถ์ถํ๋ ๊ฒ์ ์๋ํด ๋ณผ ์ ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ hook์ ์ํด์ ๋ฐํ๋ ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋๋งํ ๋ด์ฉ์ ์ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด ๋ค์๊ณผ ๊ฐ์ด useList ๊ฐ์ custom hook์ ์์ฑํ ์ ์์ต๋๋ค.
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}>
๋ค์
</button>
</div>
);
}๋ฐ์ดํฐ ํ๋ฆ์ ๋ช
์์ ์ด์ง๋ง state๋ ๋ชจ๋ ์ปดํฌ๋ํธ์์ ์ฌ์ฉํ ์ ์๋ useList custom hook ๋ด๋ถ์ ์์ต๋๋ค.
import Row from './Row.js'; import useList from './useList.js'; import { products } from './data.js'; 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}> ๋ค์ </button> </div> ); }
์ด๋ฌํ ์ ๊ทผ ๋ฐฉ์์ ๋ค๋ฅธ ์ปดํฌ๋ํธ ๊ฐ์ ํด๋น ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ ๋ ํนํ ์ ์ฉํฉ๋๋ค.