📗목차
웹 애플리케이션을 만들다 보면 다음과 같이 반복되는 코드를 작성할 때가 있다.
IterationSample.js
import React from 'react'; const IterationSample = () => { return ( <ul> <li>눈사람</li> <li>얼음</li> <li>눈</li> <li>바람</li> </ul> ); }; export default IterationSample;
코드에서 다음 형태가 계속 반복되는 것을 볼 수 있다.
<li>...</li>
지금은 li 태그 하나뿐이라 그렇게 문제가 되지 않을 거 같지만 코드가 좀 복잡해진다면 코드양은 더욱 늘어날 것이며, 파일 용량도 쓸데없이 증가하게 된다. 이는 낭비가 되고, 보여 주어야 할 데이터가 유동적이라면 이런 코드로는 절대로 관리하지 못한다.
이런 반복적인 내용을 효율적으로 보여주고 관리하는 방법을 알아보자.
앞에서 만들었던 IterarionSample 컴포넌트를 다음과 같이 수정해보자.
import React from 'react';
const IterationSample = () => {
const names = ['눈사람', '얼음', '눈', '바람'];
const nameList = names.map(name => <li>{name}</li>);
return <ul>{nameList}</ul>
};
export default IterationSample;
import React, { Component } from 'react';
import IterationSample from './IterationSample';
class App extends Component {
render() {
return (
<div>
<IterationSample />
</div>
);
}
}
export default App;
자바스크립트의 배열 객체의 내장 함수인 map 함수를 이용해서 반복되는 컴포넌트를 렌더링하였다.
원하는 대로 렌더링 되었다.
하지만 크롬 개발자 도구의 콘솔을 열어보면, "key" prop이 없다는 경고 메시지를 표시했다. key란 무엇일까?
리액트에서 key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용한다.
예를 들어 유동적인 데이터를 다룰 때는 원소를 새로 생성할 수도, 제거할 수도, 수정할 수도 있다. key가 없을 때는 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지한다. 하지만 key가 있다면 이 값을 사용하여 어떤 변화가 일어났는지 더욱 빠르게 나타날 수 있다.
key 값을 설정할 때는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설정하면 된다.
key 값은 언제나 유일해야 한다. 따라서 데이터가 가진 고윳값을 key 값으로 설정해야 한다.
예를 들어 다음과 같이 게시판의 게시물을 렌더링한다면 게시물 번호를 key 값으로 설정해야 한다.
const articleList = articles.map(article => (
<Article
title={article.title}
writer={article.writer}
key={article.id}
/>
);
하지만 앞서 만들었던 IterationSample 컴포넌트에는 이런 고유 번호가 없다. 이 때는 map 함수에 전달되는 콜백 함수의 인수인 index 값을 사용하면 된다.
다음과 같은 코드로 수정해보자.
import React from 'react';
const IterationSample = () => {
const names = ['눈사람', '얼음', '눈', '바람'];
const nameList = names.map((name, index) => <li key={index}>{name}</li>
return <ul>{namelist}</ul>
};
export default IterationSample;
이제 개발자 도구에서 더 이상 경고 메시지를 표시하지 않는다. 고유한 값이 없을 때만 index 값을 key로 사용해야 한다. index를 key로 사용하면 배열이 변경될 때 효율적으로 리렌더링하지 못한다.
지금까지 배운 개념을 응용하여 고정된 배열을 렌더링 하는 것이 아닌, 동적인 배열을 렌더링하는 것을 구현해 보겠다. 그리고 index 값을 key로 사용하면 리렌더링이 비효율적이라고 배웠는데 이러한 상황에 어떻게 고윳값을 만들 수 있는지 알아보자.
useState를 사용하여 상태를 설정하겠다.
세 가지 상태
import React, { useState } from 'react';
const IterationSample = () => {
const [names, setNames] = useState([
{ id: 1, text: '눈사람'},
{ id: 2, text: '얼음'},
{ id: 3, text: '눈'},
{ id: 4, text: '바람'}
]);
const [inputText, setInputText] = useState('');
const [nextId, setNextId] = useState(5);
const onChage = e => setInputText(e.target.value);
const nameList = names.map(name => <li key={name.id}>{name.text}</li>
return (
<>
<input value={inputText} onChange={onChange} />
<button>추가</button>
<ul>{namelist}</ul>
</>
);
};
export default IterationSample;
이번에는 map 함수를 사용할 때 key 값을 index 대신 name.id 값으로 지정해주었다.
input의 상태를 관리하기 위해 onChange 이벤트를 설정했다.
그 다음에는 버튼을 클릭했을 때 호출할 onClick 함수를 선언하여 버튼의 onClick 이벤트로 설정해보자.
import React, { useState } from 'react';
const IterationSample = () => {
const [names, setNames] = useState([
{ id: 1, text: '눈사람'},
{ id: 2, text: '얼음'},
{ id: 3, text: '눈'},
{ id: 4, text: '바람'}
]);
const [inputText, setInputText] = useState('');
const [nextId, setNextId] = useState(5);
const onChange = e => setInputText(e.target.value);
const onClick = () => {
const nextNames = names.concat({
id: nextId,
text: inputText
});
setNextId(nextId+1);
setNames(nextNames);
setInputText('');
};
const nameList = names.map(name => <li key={name.id}>{name.text}</li>);
return (
<>
<input value={inputText} onChange={onChange} />
<button onClick={onClick}>추가</button>
<ul>{nameList}</ul>
</>
);
};
export default IterationSample;
배열에서 새 항목을 추가할 때 배열의 push 함수를 사용하지 않고 concat을 사용했는데, push 함수는 기존 배열 자체를 변경해주는 반면, concat은 새로운 배열을 만들어 준다는 차이점이 있다.
리액트에서는 상태를 업데이트 할 때는 기존 상태를 그대로 두면서 새로운 값을 상태로 설정해야 한다. 이를 불변성 유지라 하는데, 불변성 유지를 해줘야 나중에 리액트 컴포넌트의 성능을 최적화할 수 있다.
이번에는 각 항목을 더블 클릭했을 때 해당 항목이 화면에서 사라지는 기능을 구현해볼 것이다. 이번에도 마찬가지로 불변성을 유지하면서 업데이트 해주어야 한다. 불변성을 유지하면서 배열의 특정 항목을 지울 때는 배열의 내장 함수 filter를 사용한다.
import React, { useState } from 'react';
const IterationSample = () => {
const [names, setNames] = useState([
{ id: 1, text: '눈사람' },
{ id: 2, text: '얼음' },
{ id: 3, text: '눈' },
{ id: 4, text: '바람' },
]);
const [inputText, setInputText] = useState('');
const [nextId, setNextId] = useState(5);
const onChange = e => setInputText(e.target.value);
const onClick = () => {
const nextNames = names.concat({
id: nextId,
text: inputText
});
setNextId(nextId+1);
setNames(nextNames);
setInputText('');
};
const onRemove = id => {
const nextNames = names.filter(name => name.id !== id);
setNames(nextNames);
};
const nameList = names.map(name => (<li key={name.id} onDoubleClick={() => onRemove(name.id)}>{name.text}</li>));
return (
<>
<input value={inputText} onChange={onChange} />
<button onClick={onClick}>추가</button>
<ul>{nameList}</ul>
</>
);
};
export default IterationSample;
반복되는 데이터를 렌더링하는 방법을 배우고, 이를 응용하여 유동적인 배열을 다루어 보았다. 컴포넌트 배열을 렌더링할 때는 key 값 설정에 항상 주의해야 한다. 또 key 값은 언제나 유일해야 한다. key 값이 중복된다면 렌더링 과정에서 오류가 발생한다.
상태 안에서 배열을 변형할 때 배열에 직접 접근하여 수정하는 것이 아니라 concat, filter 등의 배열 내장 함수를 사용하여 새로운 배열을 만든 후 이를 새로운 상태로 설정해줘야 한다는 점을 명심하자.
참고문헌
김민준,「리액트를 다루는 기술 :실무에서 알아야 할 기술은 따로 있다!」, 길벗, 개정판[실은 2판] 2019 (개정판)