..

Search

22) 배열 렌더링

배열 렌더링


map()을 활용한 배열 렌더링

React 앱을 구현하다 보면 데이터에서 여러 개의 유사한 컴포넌트들을 가져와 표시해야 할 경우가 종종 발생합니다. 이 때 자바스크립트 배열 객체의 map() 메소드를 사용하면 반복되는 코드를 간결하게 구현할 수 있습니다.

 

map() 메소드는 인수로 전달된 콜백 함수(callback function)를 배열의 각 요소에 대해 한 번씩 순서대로 호출한 후, 그 반환값들을 모아 새로운 배열로 생성하여 반환합니다.

map() 문법

arr.map(callback(currentValue[, index[, array]])[, thisArg])

 

map() 메소드의 콜백 함수는 호출될 때 처리할 현재 요소의 값(currentValue), 처리할 현재 요소의 인덱스(index), 그리고 map() 메소드를 호출한 원본 배열(array)까지 총 3개의 인수를 전달 받습니다.

 

map() 메소드는 호출한 원본 배열의 값을 변형하지는 않지만, 콜백 함수에 의해서 변형될 수는 있습니다.

 

다음 예제는 배열의 각 요소를 리스트 아이템으로 렌더링하는 예제입니다.

예제(List.js)
const bucketItems = [
  "세계 일주 여행",
  "스카이 다이빙",
  "오로라 구경하기",
  "마라톤 완주"
];

const List = () => {
  const bucketList = bucketItems.map((bucketItem) => <li>{bucketItem}</li>);
  return (
    <>
      <h1>버킷 리스트</h1>
      <ul>{bucketList}</ul>
    </>
  );
};

export default List;

 

위의 예제는 다음과 같은 순서대로 동작합니다.

1. bucketItems 배열의 각 요소에 대해 map() 메소드를 호출하여 각각의 <li>엘리먼트를 생성합니다.

2. 생성된 엘리먼트들을 새로운 JSX 엘리먼트 리스트인 bucketList에 저장합니다.

3. 마지막으로 <ul>태그로 감싼 bucketList를 반환합니다.

 

하지만 위의 예제를 실행하면 콘솔에서 리스트의 각 자식 요소들은 유일한 “key” prop을 가지고 있어야만 한다는 경고가 발생하는 것을 확인할 수 있습니다.

Console

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

Check the render method of `List`. See

https://reactjs.org/link/warning-keys

for more information.

at li

at List


key prop 설정

React에서는 엘리먼트 리스트를 만들 때 각 아이템마다 고유한 key를 지정해야 합니다. 이러한 key를 통해 해당 리스트에 저장된 아이템 간의 식별을 안정적으로 수행하며, 어떤 아이템이 변경, 추가 또는 삭제되었는지를 빠르게 파악할 수 있습니다. 잘 선택된 key는 React가 리스트에 무슨 일이 일어났는지를 파악하고, DOM 트리를 정확하게 업데이트하는 데 많은 도움을 줄 수 있습니다.

 

key 값은 같은 리스트에 포함된 아이템 사이에서만 고유한 값을 가지면 됩니다. 즉, 전체 애플리케이션이나 단일 컴포넌트 전체를 모두 통틀어 고유한 값일 필요는 없습니다. 따라서 다른 리스트에서 같은 key 값을 사용하는 것은 전혀 문제가 되지 않습니다.

 

일반적으로 key는 해당 데이터가 가지고 있는 고유한 값(id)을 사용하는 것이 가장 좋습니다. 예를 들어, 데이터베이스로부터 가지고 온 데이터라면 DB의 key 값이나 id 값을 사용하는 것이 좋으며, 게시판 데이터라면 게시물 id 등을 key로 사용하는 것이 좋습니다.

 

Math.random()과 같이 랜덤한 값을 key로 사용해서는 안 됩니다. key는 React가 아이템을 추가, 제거 또는 다시 정렬할 시기를 결정할 수 있도록 리렌더링 시 “안정적으로 식별 가능”한 값이어야 합니다.

 

만약 렌더링한 엘리먼트에 대한 안정적인 고윳값이 전혀 없다면, 마지막 수단으로 다음 예제처럼 리스트의 index를 key로 사용할 수 있습니다.

index 값을 key로 사용한 예시
const bucketList = bucketItems.map((bucketItem, index) => (
    <li key={index}>{bucketList}</li>
  ));

 

React에서는 리스트 아이템에 key를 명시적으로 지정하지 않으면, 기본적으로 index 값을 key로 사용합니다. 하지만 index 값을 key로 사용하는 것은 애플리케이션의 성능 저하나 컴포넌트의 state와 관련된 문제가 발생할 수 있기 때문에 권장하지 않습니다.

 

다음 예제는 map() 메소드에 사용할 수 있도록 배열의 각 요소에 id 속성을 추가한 예제입니다.

예제(List.js)
const bucketItems = [
  { id: 0, text: "세계 일주 여행" },
  { id: 1, text: "스카이 다이빙" },
  { id: 2, text: "오로라 구경하기" },
  { id: 3, text: "마라톤 완주" }
];

const List = () => {
  const bucketList = bucketItems.map((bucketItem) => (
    <li key={bucketItem.id}>{bucketItem.text}</li>
  ));
  return (
    <>
      <h1>버킷 리스트</h1>
      <ul>{bucketList}</ul>
    </>
  );
};

export default List;

 

예제를 실행하면 이제 더 이상 콘솔에서 경고가 발생하지 않음을 확인할 수 있습니다.


아이템 추가 기능 구현

앞선 예제에 state를 활용하여 배열에 새로운 요소를 추가하는 기능을 구현해 봅시다.

 

React Hook은 함수 컴포넌트 내부에서만 호출할 수 있기 때문에 useState를 사용하기 위해서 배열을 List 컴포넌트 내부로 이동시킵니다.

 
const [inputText, setValue] = useState("");
//...
const onChange = (e) => setValue(e.target.value);
//...
<input value={inputText} onChange={onChange} />
//...

 

useState와 onChange 이벤트를 사용하여 사용자가 <input>요소의 입력 필드에 작성하는 내용을 그대로 inputText 변수에 업데이트할 수 있도록 다음과 같은 코드를 추가합니다.

 
const [id, setId] = useState(4); // 이제부터 추가되는 아이템의 key 값은 4부터 시작함.
//...
const onClick = () => {
  const newItems = items.concat({
    id: id,
    name: inputText
  });
  setItems(newItems);
  setId(id + 1);
  setValue("");
};
//...
<button onClick={onClick}>추가하기</button>
//...

 

우리는 앞서 React에서는 참조 타입인 객체나 배열의 경우 반드시 불변성(Immutability)이 지켜져야만 한다고 배웠습니다. 따라서 배열에 새로운 요소를 추가할 때는 배열 객체의 push() 메소드를 사용해서는 안 됩니다. push() 메소드는 메소드를 호출한 원본 배열의 마지막부터 새로운 요소를 추가하므로 바로 이 불변성을 위반하게 됩니다.

 

이와 같은 이유로 React에서는 push() 메소드 대신 concat() 메소드를 사용하여 새로운 요소가 추가된 새로운 배열을 생성하고, setItems() 함수를 호출하여 state를 업데이트해야만 합니다.

 

concat() 메소드는 메소드를 호출한 원본 배열의 마지막부터 인수로 전달받은 값들을 순서대로 추가한 새로운 배열을 생성하여 반환하므로, 원본 배열의 불변성이 지켜지게 됩니다.

concat() 문법

array.concat([value1[, value2[, ...[, valueN]]]])

 

예제(List.js)
import { useState } from "react";

const List = () => {
  const [items, setItems] = useState([
    { id: 0, name: "세계 일주 여행" },
    { id: 1, name: "스카이 다이빙" },
    { id: 2, name: "오로라 구경하기" },
    { id: 3, name: "마라톤 완주" }
  ]);
  const [inputText, setValue] = useState("");
  const [id, setId] = useState(4); // 이제부터 추가되는 아이템의 key 값은 4부터 시작함.

  const onChange = (e) => setValue(e.target.value);

  const onClick = () => {
    const newItems = items.concat({
      id: id,
      name: inputText
    });
    setItems(newItems);
    setId(id + 1);
    setValue("");
  };

  const bucketList = items.map((item) => <li key={item.id}>{item.name}</li>);

  return (
    <>
      <h1>버킷 리스트</h1>
      <input value={inputText} onChange={onChange} />
      <button onClick={onClick}>추가하기</button>
      <ul>{bucketList}</ul>
    </>
  );
};

export default List;


아이템 제거 기능 구현

마지막으로 리스트에서 제거하고자 하는 아이템 위에서 마우스를 우클릭하면 배열에서 요소를 제거하는 기능을 구현해 봅시다.

 

배열 객체의 filter() 메소드는 배열 내 각 요소에 대해 전달받은 콜백 함수를 호출하여, true 값을 반환하는 모든 요소들을 새로운 배열로 생성하여 반환합니다.

filter() 문법

arr.filter(callback(element[, index[, array]])[, thisArg])

 

이와 같은 filter() 메소드를 활용하면 특정 조건을 만족하는 요소들을 선택하거나, 특정 조건을 만족하는 요소들만을 손쉽게 제거할 수 있습니다.

예제(List.js)
//...
const onRemove = (id) => {
  const newItems = items.filter((name) => name.id !== id);
  setItems(newItems);
};
//...
const bucketList = items.map((item) => (
    <li
      key={item.id}
      onContextMenu={(e) => {
        onRemove(item.id);
        e.preventDefault();
      }}
    >
      {item.name}
    </li>
  ));
//...

 

onRemove() 함수에서는 filter() 메소드를 사용하여 인수로 전달받은 id 값에 해당하는 요소만을 제외한 새로운 배열을 생성합니다. 이렇게 생성된 배열을 setItems() 함수에 전달하여 해당 요소가 제거된 새로운 배열을 newItems에 업데이트합니다.

 

자바스크립트에서 마우스 우클릭 동작은 onContextMenu 이벤트로 제어할 수 있습니다. 하지만 마우스를 우클릭하게 되면 우클릭 이벤트와 함께 마우스 우클릭의 기본 동작인 컨텍스트 메뉴(context menu)도 함께 실행됩니다. 따라서 preventDefault() 메소드를 명시적으로 호출하여 마우스 우클릭의 기본 동작을 방지하고 있습니다.


연습문제