본문 바로가기

React

컴포넌트 반복

1. 컴포넌트의 반복 이란?


웹 애플리케이션을 만들다 보면 다음과 같이 반복되는 코드를 작성할 때가 있습니다.

import React from "react";

function App() {
  return (
    <ul>
      <li>눈사람</li>
      <li>얼음</li>
      <li>바람</li>
    </ul>
  )
}

export default App;


코드에서 다음 형태가 계속 반복되는 것을 볼 수 있습니다.

  <li>...</li>

지금은 li 태그 하나뿐이라 그렇게 문제가 되지는 않을 것 같습니다. 하지만 코드가 좀 더 복잡하다면 어떨까요? 코드양은 더더욱 늘어날 것이며, 파일 용량도 쓸데없이 증가하겠죠.이는 낭비입니다. 또 보여 주어야 할 데이터가 유동적이라면 이런 코드로는 절대로 관리하지 못합니다.


2.  자바스크립트 배열의 map() 함수
자바스크립트 배열 객체의 내장 함수인 map 함수를 사용하여 반복되는 컴포넌트를 렌더링 할 수 있습니다.map 함수는 파라미터로 전달된 함수를 사용해서 배열 내 각 요소를 원하는 규칙에 따라 변환한 후 그 결과로 새로운 배열을 생성합니다.

2-1 문법
arr.map(callback, [thisArg])

이 함수의 파라미터는 다음과 같습니다.

callback: 새로운 배열의 요소를 생성하는 함수로 파라미터는 다음 세 가지 입니다.
- currentValue: 현재 처리하고 있는 요소
- index: 현재 처리하고 있는 요소의 index 값
- array: 현재 처리하고 있는 원본 배열
thisArg(선택 항목): callback 함수 내부에서 사용할 this 레퍼런스

2-2 예제

import React from "react";

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

export default App;


이 코드를 렌더링 하고 개발자 도구의 콘솔을 확인해 보면 이러한 에러가 나옵니다.

map에 key값을 주지 않았을때 에러


3. key
리액트에서 key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용합니다. 예를 들어 유동적인 데이터를 다룰 때는 원소를 새로 생성할 수도, 제거할 수도, 수정할 수도 있죠. key가 없을 때는 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지합니다. 하지만 key가 있다면 이 값을 사용하여 어떤 변화가 일어났는지 더욱 빠르게 알아낼 수 있습니다.

3-1. 예제

import React from 'react'

function App() {
  const articleList = articles.map((article) => (
    <Article title={article.title} writer={article.writer} key={article.id} />
  ))
  return <div>{articleList}</div>
}

export default App


key 값을 설정할 때는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설정하면 됩니다. key 값은 언제나 유일해야 합니다. 따라서 데이터가 가진 고윳값을 key로 설정해야 합니다. 위의 예제는 고유한 key값을 가진 경우 입니다.

import React from "react";

function App() {
  const names = ["눈사람", "얼음", "눈", "바람"];
  const nameList = names.map((name, index) => <li key={index}>{name}</li>);
  return <ul>{nameList}</ul>;
}

export default App;


위의 예제는 고유 번호가 없는 경우입니다. 이때는 map 함수에 전달되는 콜백 함수의 인수인 index값을 사용하면 됩니다.
하지만 이런경우에는 렌더링이 비효율적 입니다.

4. 응용
이제 고정된 배열이 아닌 동적인 배열을 렌더링하도록 합니다. 즉 데이터를 추가하고 삭제하는 것이 가능하도록 예제를 변경해봅니다. 또한 index 값이 아닌 고윳값을 만들어 key로 사용하는 법도 알아봅니다.

4-1 초기 상태 설정
컴포넌트에서 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 nameList = names.map(name => <li key={name.id}>{name.text}</li>);
  return <ul>{nameList}</ul>;
};

export default IterationSample;


이전과 달리 names 에 text 와 고유의 id 를 담고 있는 객체들의 배열을 넣어줍니다. 그리고 새로 추가할 데이터를 입력받기 위해 inputText 를 추가했고, nextId 를 통해 추가될 데이터의 아이디를 계산해 주도록 했습니다.

4-2. 추가 기능 구현

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;


onChange 함수를 통해 inputText 를 입력받도록 했습니다.

그리고 onClick 함수에서는 배열의 내장 함수 concat 을 사용하여 새로운 항목을 추가한 배열을 만들어준 다음, 새로 만들어진 nextNames 를 setNames 로 상태를 업데이트해 줍니다.

새 항목을 추가할 때 배열의 push 함수가 아니라 concat 을 사용하는 이유가 중요한데, push 함수는 기존 배열에 항목을 추가하는 것으로 기존 배열 자체를 변경하는 반면, concat 함수는 기존 배열에 항목을 추가한 새로운 배열을 만들어 줍니다.

리액트에서 상태를 업데이트할 때는 기존 상태를 그대로 두면서 새로운 값을 상태로 설정해야 합니다.

이를 불변성 유지라고 하며, 이는 리액트 컴포넌트의 성능을 최적화하는데 중요한 개념입니다.

 

4-3. 제거 기능 구현하기
이번에는 각 항목을 더블클릭했을 때 해당 항목이 사라지는 기능을 구현해 보는데, 불변성을 유지하면서 배열의 특정 항목을 지울 때는 배열의 내장 함수 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;


HTML 요소를 더블클릭할 때 사용하는 이벤트의 이름은 onDoubleClick 입니다. onRemove 라는 함수를 구현해서 각 li 요소에 이벤트로 등록해줍니다. onRemove 함수에서는 클릭할 때 클릭이 발생한 li 요소의 id 를 매개변수로 받아 해당 id 를 가진 name 객체를 제거하고, 필터링 된 배열 nextNames 로 names 를 업데이트 합니다.

5. 정리
반복되는 데이터를 렌더링하는 방법을 배우고, 이를 응용하여 유동적인 배열을 다루어 보았습니다. 컴포넌트 배열을 렌더링할 때는 key 값 설정에 항상 주의해야 합니다. 또 key 값은 언제나 유일해야 합니다. key 값이 중복된다면 렌더링 과정에서 오류가 발생합니다.상태 안에서 배열을 변형할 때는 배열에 직접 접근하여 수정하는 것이 아니라 concat, filter 등의 배열 내장 함수를 사용하여 새로운 배열을 만든 후 이를 새로운 상태로 설정해 주어야 한다는 점을 명심하세요.

 

[출처] https://codenamedev.tistory.com/3