2021-05-12 수요일
리액트에서 배열로 JSX를 Render할 경우 key가 필요한데
나는 UUID를 리턴해주는 getUuid라는 유틸 함수를 만들어 사용하고 있다.
그렇게 쓰던 도중 원인모를 이미지 깜빡거림에 두 시간정도 삽질을 했는데
원인은 key에 사용한 getUuid에 있었다.
내가 uuid를 key로 사용했던 방식은 아래와 같은 방식이었다.
function Missions() {
const missions = [...];
return (
<div className="missions">
{missions.map(({ imgSrc, title, content, key }) => (
<div className="mission-item" key={getUuid()}> <img src={imgSrc} alt={title} className="mission-item-img" />
<h2 className="mission-item-title">{title}</h2>
<p className="mission-item-content">{content}</p>
</div>
))}
</div>
);
}그리고 해당 Component를 사용하는 Parent Component가 있었다.
그 Parent Component에 state가 있고 그 state가 변경될 때마다 Child Component인 Missions가 Re-render 되면서 이미지가 깜빡이는 현상이 발생되고 있었다.
export default function Parent() {
const [data, setData] = useState('-');
useEffect(() => {
setInterval(() => {
setData(getUuid());
}, 1000);
}, []);
return (
<main>
<div>Home Data: {data}</div>
<Missions />
</main>
);
}

Parent의 state가 바뀌어 Parent Component 가 재호출 되면서 되면서 그 Child인 Missions Component도 함수가 다시 실행이 된다.
그 때 getUuid 함수 호출이 다시 되면서 새로운 UUID로 변경되고 key가 이전과 다르기 때문에 Re-render가 발생한다.
내가 시도한 방법들과 결과를 공유한다.
첫 번째 방법은 render쪽에서 getUuid를 호출하지 않고 변수 선언할 때 key라는 속성을 만들어서 UUID를 할당해 주는 방법이다.
이 방법은 해결이 되지 않는다.
function Missions() {
const missions = [
{
key: getUuid(), ...
},
...
];
return (
<div className="missions">
{missions.map(({ imgSrc, title, content, key }) => (
<div className="mission-item" key={key}> <img src={imgSrc} alt={title} className="mission-item-img" />
<h2 className="mission-item-title">{title}</h2>
<p className="mission-item-content">{content}</p>
</div>
))}
</div>
);
}이 방법은 위의 원인 부분의 내용을 잘 생각해보면 해결될 수가 없다.
Missions Component가 다시 호출되고 missions 변수도 다시 생성되면서 getUuid가 다시 실행되는 것은 똑같기 때문이다.
두 번째 방법은 첫 번째 방법에 추가로 missions 변수를 useMemo를 사용하여 선언해 주는 방법이다.
useMemo를 사용하면 의존성 배열에 넣은 값이 바뀌기 전까진 해당 변수가 다시 만들어지지 않는다.
이 방법을 사용하면 해결이 된다.
function Missions() {
const missions = useMemo( () => [
{
key: getUuid(),
...
},
...
],
[]
);
return (
<div className="missions">
{missions.map(({ imgSrc, title, content, key }) => (
<div className="mission-item" key={key}>
<img src={imgSrc} alt={title} className="mission-item-img" />
<h2 className="mission-item-title">{title}</h2>
<p className="mission-item-content">{content}</p>
</div>
))}
</div>
);
}세 번째 방법은 Component가 다시 호출 되어도 변하지 않을 값을 key에 사용하는 것이다.
객체의 속성 중 적당한 값을 골라서 key에 넣어주면 된다. 중복될 가능성이 없는 값이 좋다.
아니면 두 가지 이상의 속성을 조합해서 넣어줘도 된다. (e.g. key={title+content})
JSON.stringify로 아예 객체를 JSON 문자열로 만들어서 key로 사용하는 방법도 있다. (이 방법은 key가 굉장히 길어질 수 있는데 어떤 문제점이 있을지는 모르겠다.)
UUID를 사용하지 않아도 된다.
이 방법을 사용하면 해결이 된다.
function Missions() {
const missions = [
{...},
...
];
return (
<div className="missions">
{missions.map((mission) => (
<div className="mission-item" key={JSON.stringify(mission)}> <img
src={mission.imgSrc}
alt={mission.title}
className="mission-item-img"
/>
<h2 className="mission-item-title">{mission.title}</h2>
<p className="mission-item-content">{mission.content}</p>
</div>
))}
</div>
);
}참고로 배열의 index 값을 key로 사용하는 것은 안 좋은 방법이라고 한다. (참고: https://medium.com/sjk5766/react-배열의-index를-key로-쓰면-안되는-이유-3ce48b3a18fb)
네 번째 방법은 첫 번째 방법에 추가로 missions 변수를 아예 Component 외부에서 선언하는 방법이다.
이렇게 하면 Component가 다시 호출되어도 변수는 이미 Component 바깥에 선언해 뒀기 때문에 변수의 내용은 변경되지 않는다.
이 방법을 사용하면 해결이 된다.
const missions = [ { ... key: getUuid(), }, ...];
function Missions() {
return (
<div className="missions">
{missions.map(({ imgSrc, title, content, key }) => (
<div className="mission-item" key={key}>
<img src={imgSrc} alt={title} className="mission-item-img" />
<h2 className="mission-item-title">{title}</h2>
<p className="mission-item-content">{content}</p>
</div>
))}
</div>
);
}Component에서 사용되지만 state로 사용하지 않을(변경되지 않을) 변수를 선언할 때 useMemo를 사용하는 것이 좋은 방법인지, 아예 Component 외부에 선언해서 사용하는 것이 좋은 방법인지 고민을 해봤다.
어차피 변경되지 않을 값이라는 것을 개발자는 알고 있으니 괜히 useMemo를 사용해서 어떤 특정 자원(메모리, 의존성 배열 감시)을 소모하는 것 보다는 외부에 선언하는 것이 자원 소모를 줄일 수 있고 더 명시적인 코드가 될 것 같다고 생각하였다.
getUuid 함수의 내용
import { v4 as uuid } from 'uuid';
export function getUuid() {
return uuid();
}