컴포넌트 반복


컴포넌트 배열

리액트 프로젝트를 작성하다 보면 반복되는 코드를 작성할 때가 있습니다.

  • 반복되는 코드 예시
    IterationSample.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import React from 'react';

    const IterationSample = () => {
    return (
    <ul>
    <li>눈사람</li>
    <li>얼음</li>
    <li>눈</li>
    <li>바람</li>
    </ul>
    );
    };

    export default IterationSample;

위는 하나의 예시이며 <li> 태그 하나만 반복되는 것이 아니라 코드가 조금더 복잡해 진다면 코드양이 더더욱 늘어날 것이며 관리하기 힘들어 집니다.

리액트에서는 자바스크립트의 Map 메서드를 사용하여 더욱 간단하게 표현할 수 있습니다.

IterationSample.js
1
2
3
4
5
6
7
8
9
import React from 'react';

const IterationSample = () => {
const names = ['눈사람', '얼음', '눈', '바람'];
const nameList = names.map(name => <li>{name}</li>);
return <ul>{nameList}</ul>;
};

export default IterationSample;
  • 문자열로 구성된 배열을 선언합니다.
  • 그 배열 값을 사용하여 <li>...</li> JSX 코드로 된 배열을 새로 생성한 후 nameList에 담습니다.

App 컴포넌트에서 렌더링 시

App 컴포넌트에서 IterationSample.js 컴포넌트를 불러와 렌더링 하면

브라우저 화면에는 잘 표시되는 듯 보이나 개발자 도구에서 경고 메세지가 표시됩니다.

Warning: Each child in a list should have a unique "key" prop.

“key” prop이 없다는 경고 메서지입니다.

key가 무엇인지 알아봅시다.


key

리액트에서 key는 컴포넌트 배열을 렌더링했을 때 어떤 요소에 변동이 있었는지 알아내려고 사용합니다.

key가 없을 때는 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지합니다.

하지만 key가 있다면 이 값을 사용하여 어떤 변화가 일어났는지 더욱 빠르게 알아낼 수 있습니다.


key 설정

key 값을 설정할 때는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설정하면 됩니다.

  • key 값은 언제나 유일해야 합니다. (따라서 데이터가 가진 고유값을 key 값으로 설정해야 합니다.)

예를 들어 게시판의 게시물을 렌더링한다면 게시물 번호를 key 값으로 설정해야 합니다.

key 값 예시
1
2
3
4
5
6
7
const articleList = articles.map(article) => (
<Article
title={article.title}
writer={article.writer}
key={article.id}
/>
);

하지만 앞에서 만든 IterationSample 예제 컴포넌트에는 이런 고유 번호가 없습니다.

이때는 map 함수에 전달되는 콜백 함수의 인수인 index 값을 사용하면 됩니다.

IterationSample.js
1
2
3
4
5
6
7
8
9
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로 사용하면 리렌더링이 비효율적이라 하였는데, 이러한 상황에 고유값을 만드는 방법도 알아봅니다.


초기 상태 설정

IterationSample 컴포넌트에서 useState를 사용하여 3가지 상태를 설정합니다.

  1. 데이터 배열

  2. 텍스트를 입력할 수 있는 input 상태

  3. 데이터 배열에서 새로운 항목을 추가할 때 사용할 고유 id 상태

우선 배열을 작성할 때 객체 형태로 이루어진 배열로 만들어 문자열과 고유 id를 작성합니다.

IterationSample.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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); // 새 항목을 추가할 때 사용할 id

const nameList = names.map((name) => <li key={name.id}>{name.text}</li>);
return <ul>{nameList}</ul>;
};

export default IterationSample;

이번에는 map 함수를 사용할 때 key 값을 index 대신 name.id 값으로 지정해 주었습니다.


데이터 추가 기능 구현

새로운 이름을 등록할 수 있는 기능을 구현해 봅니다.

ul 태그 상단에 input 과 button을 렌더링하고 상태를 관리합니다.

IterationSample.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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); // 새 항목을 추가할 때 사용할 id

const onChange = (e) => setInputText(e.target.value);
const onClick = () => {
const nextNames = names.concat({
id: nextId, //nextId 값을 id로 설정
text: inputText, //input 안에 작성된 값
});
setNextId(nextId + 1); //nextId 값에 1을 더해줌
setNames(nextNames); //names 값을 업데이트
setInputText(''); // inputText를 비움
};
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;
  • onClick 함수에서 concat을 사용하여 새로운 항목을 추가한 배열을 만들고, setNames를 통해 상태를 업데이트 해줬습니다.
  • 배열에 새 항목을 추가할 때 push메서드 대신 concat을 사용한 이유 :
    push 메서드는 기본 배열 자체를 변경해 주는 반면
    concat 메서드는 새로운 배열을 만들어 준다는 차이점이 있습니다.

    리액트에서 상태를 업데이트할 때는 기존 상태를 그대로 두면서 새로운 값을 상태로 설정해야 합니다.
    이를 불변성 유지라고 하며, 불변성 유지를 해줘야 리액트 컴포넌트의 성능을 최적화할 수 있습니다.


데이터 제거 기능 구현

각 항목을 더블클릭했을 때 해당 항목이 화면에서 사라지는 기능을 구현해 봅니다.
이번에도 마찬가지로 불변성을 유지하면서 업데이트 해줍니다.

불변성을 유지하면서 배열의 특정 항목을 제거할 때는 filter 메서드를 사용합니다.

HTML 요소를 더블클릭할 때 사용하는 이벤트는 onDoubleClick입니다.
onRemove 라는 임의 메서드를 filter를 사용하여 만들어 보겠습니다.

IterationSample.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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); // 새 항목을 추가할 때 사용할 id

const onChange = (e) => setInputText(e.target.value);
const onClick = () => {
const nextNames = names.concat({
id: nextId, //nextId 값을 id로 설정
text: inputText, //input 안에 작성된 값
});
setNextId(nextId + 1); //nextId 값에 1을 더해줌
setNames(nextNames); //names 값을 업데이트
setInputText(''); // inputText를 비움
};
const onRemove = (id) => { // 임의 메서드 onRemove 작성
const nextNames = names.filter((name) => name.id !== id);
setNames(nextNames);
};
const nameList = names.map((name) => ( // 더블클릭시 onRemove 메서드 실행
<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;
  • <li> 태그 요소를 더블클릭시 onRemove 메서드를 실행하고 해당 배열의 id 값을 파라미터로 넘겨줍니다.
  • onRemove 메서드는 filter를 사용하여 더블클릭한 요소의 id값과 다른 요소들만 true를 반환하도록 하여, true를 반환한 요소들만 모아 새 배열로 반환하고 nextNames에 할당합니다.
  • setNames(nextNames)로 업데이트 합니다.