누구든지 하는 리액트 7편: 배열 다루기 (1) 생성과 렌더링


이 튜토리얼은 10편으로 이뤄진 시리즈입니다. 이전 / 다음 편을 확인하시려면 목차를 확인하세요.

이번에는 리액트 프로젝트에서 배열을 다루는 방법을 알아보겠습니다. 리액트에서는 배열을 다룰 때 평상시에 하던것 처럼 하시면 안 됩니다. 데이터 추가의 경우, 자바스크립트에서 배열을 다뤄보신분이라면 그냥 배열에 데이터를 추가할 때, push 를 사용하니까 this.state.array.push('some value'); 이런식으로 하면 되겠지? 라고 생각하실 수 있는데요, 리액트애서는 state 내부의 값을 직접적으로 수정하면 절대로 안됩니다. 이를 불변성 유지라고 하는데요, push, splice, unshift, pop 같은 내장함수는 배열 자체를 직접 수정하게 되므로 적합하지 않습니다. 그 대신에, 기존의 배열에 기반하여 새 배열을 만들어내는 함수인 concat, slice, map, filter 같은 함수를 사용해야합니다.

리액트에서 불변성 유지가 중요한 이유는 불변성을 유지해야, 리액트에서 모든것들이 필요한 상황에 리렌더링 되도록 설계 할 수 있고, 그렇게 해야 나중에 성능도 최적화 할 수 있기 때문입니다. 이것이 왜 그런건지에 대해서는, 우선 구현을 해보고 나중에 설명을 해드리겠습니다.

데이터 추가

우리 애플리케이션의 상태 데이터는 App 컴포넌트 에서 관리하겠습니다. 나중에 가면 상태를 컴포넌트에서 분리하여 따로 관리하는 방법도 알아볼텐데요, 일단은 지금은 리액트 기초에 집중을 해보겠습니다.

App 컴포넌트의 state 에 information 이라는 배열을 만들고, 그 안에 배열의 기본값들인 샘플 데이터 두개를 추가하겠습니다.

각 전화번호 정보는 다음과 같은 형식으로 담겠습니다.

{
  id: 0,
  name: '이름',s
  phone: '010-0000-0000'
}

여기서 id 값은 각 데이터를 식별하기 위함입니다. 그리고 이 값은 데이터를 추가할때마다 숫자를 1씩 더해주겠습니다.

자, App 코드를 다음과 같이 작성해보세요.

// file: src/App.js
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';

class App extends Component {
  id = 2
  state = {
    information: [
      {
        id: 0,
        name: '김민준',
        phone: '010-0000-0000'
      },
      {
        id: 1,
        name: '홍길동',
        phone: '010-0000-0001'
      }
    ]
  }
  handleCreate = (data) => {
    const { information } = this.state;
    this.setState({
      information: information.concat({ id: this.id++, ...data })
    })
  }
  render() {
    const { information } = this.state;
    return (
      <div>
        <PhoneForm
          onCreate={this.handleCreate}
        />
        {JSON.stringify(information)}
      </div>
    );
  }
}

export default App;

id 값의 경우에는, 컴포넌트의 일반 클래스 내부 변수로서 선언해주었습니다. 컴포넌트 내부에서 필요한 값 중에서, 렌더링 되는것과 상관이 없는것들은 굳이 state 에 넣어줄 필요가 없습니다.

render 함수에서는 information 값을 문자열로 변환하여 보여주었습니다. 잠시 후에는 이 데이터를 컴포넌트 형태로 렌더링해보겠습니다.

일단 코드를 저장해보고 새 데이터를 입력해보세요.

데이터가 잘 나타났나요?

데이터 렌더링

자, 이제는 위 배열을 컴포넌트로 변환해서 바꿔주겠습니다. 이 튜토리얼 초반부에서도 언급했지만, 리액트를 다루는건 자바스크립트를 사용하는거랑 매우 비슷합니다. 컴포넌트를 여러개 렌더링 하기 위해서는, 앵귤러 뷰 처럼 디렉티브 같은걸 사용 할 필요 없이, 그냥 자바스크립트 배열의 내장함수인 map 을 사용하면 됩니다.

map 함수 알아보기

자바스크립트에 익숙한 사람이라면 불필요한 설명일 수도 있겠지만, 그렇지 않은 분들을 위해서 이 함수에 대해서 간단히 알아보겠습니다.

예를들어 다음과 같은 배열이 있다고 가정해봅시다.

const a = [1,2,3,4,5];

만약에 여러분들이 이 배열을 가지고, 내부에 있는 원소들에 2씩 곱하고 싶다고 가정해봅시다. 어떻게 하실건가요? 자바스크립트의 기초만 알고있다면 아마 이런 방법을 선택 하실 수도 있습니다.

const a = [1,2,3,4,5];
const b = [];

b.forEach(number => b.push(number * 2));

하지만, forEach 대신에 map 을 사용하시면 조금 더 쉽게 해결 할 수 있습니다.

const a = [1,2,3,4,5];
const b = a.map(number => number * 2);

어느정도 감이 잡혔나요? 조금 더 자세히 알고 싶다면 MDN 의 Array.prototype.map() 를 읽어보세요.

컴포넌트 만들기

우리는 두개의 컴포넌트를 만들겠습니다.

  • PhoneInfo: 각 전화번호 정보를 보여주는 컴포넌트입니다.
  • PhoneInfoList: 여러개의 PhoneInfo 컴포넌트들을 보여줍니다.

일단 PhoneInfo 부터 만들어보겠습니다.

// file: src/components/PhoneInfo.js
import React, { Component } from 'react';

class PhoneInfo extends Component {
  static defaultProps = {
    info: {
      name: '이름',
      phone: '010-0000-0000',
      id: 0
    }
  }
  
  render() {
    const style = {
      border: '1px solid black',
      padding: '8px',
      margin: '8px'
    };

    const {
      name, phone, id
    } = this.props.info;
    
    return (
      <div style={style}>
        <div><b>{name}</b></div>
        <div>{phone}</div>
      </div>
    );
  }
}

export default PhoneInfo;

우리는 info 라는 객체를 props 로 받아와서 렌더링 해줄것입니다. 그런데, 우리가 실수로 info 값을 전달해주는것을 까먹게 된다면 컴포넌트가 크래쉬 될 것입니다. info 가 undefined 일 때에는 비구조화 할당을 통해 내부의 값을 받아올 수 없기 때문입니다.

그렇기 때문에 defaultProps 를 통하여 info 의 기본값을 설정해주었습니다.

그 다음에는, PhoneInfoList 컴포넌트를 만들어봅시다.

// src/components/PhoneInfoList.js
import React, { Component } from 'react';
import PhoneInfo from './PhoneInfo';

class PhoneInfoList extends Component {
  static defaultProps = {
    data: []
  }

  render() {
    const { data } = this.props;
    const list = data.map(
      info => (<PhoneInfo key={info.id} info={info}/>)
    );

    return (
      <div>
        {list}    
      </div>
    );
  }
}

export default PhoneInfoList;

이 컴포넌트에서는 data 라는 배열을 가져와서 map 을 통하여 JSX 로 변환을 해줍니다. 이 과정에서, key 라는 값도 설정이 되었는데요, 여기서 key 는 리액트에서 배열을 렌더링을 할 때 꼭 필요한 값입니다. 리액트는 배열을 렌더링 할 때 값을 통하여 업데이트 성능을 최적화하는데요, 한번 다음 예시를 살펴보세요.

<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>

만약에 key 를 부여하지 않으면, 배열의 index 값이 자동으로 key 로 설정이되는데요, 이 때 여기서 B와 C 사이에 X를 집어넣는다고 가정해봅시다. key 가 배열의 인덱스로 설정된다면 이렇게됩니다.

<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>C</div>
<div key={3}>D</div>

배열의 인덱스가 key 값으로 사용됐습니다.

<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>X</div> [C -> X]
<div key={3}>D -> C</div> [D -> C]
<div key={4}>D</div> [새로 생성됨]

보시면, 굉장히 비효율적이죠? 사실상 중간에 끼워넣기만 하면 되는건데, 배열의 index 를 key 로 사용하게되어 중간에 값이 들어가면 index 도 함께 바뀌어 버리게 되니, X 아래로 값이 다 바뀌게 되버리죠.

key 를 배열의 index 값으로 사용하는게 아니라, 우리가 데이터를 추가 할 때마다 고정적인 고유 값을 부여해주면, 리액트가 변화를 감지해내고 업데이트를 하게 될 떄 조금 더 똑똑하게 처리 할 수 있게됩니다.

<div key={0}>A</div>
<div key={1}>B</div>
<div key={2}>C</div>
<div key={3}>D</div>

이 번에, 다시 B 와 C 사이에 다시 X 를 넣어보겠습니다. 이번에는 key 값은 고정된 고유값입니다.

<div key={0}>A</div>
<div key={1}>B</div>
<div key={5}>X</div> [새로 생성됨]
<div key={2}>C</div> [유지됨]
<div key={3}>D</div> [유지됨]

결국 새로운 DOM 은 하나만 생성되고, 나머지는 그대로 유지됩니다.
key 값은 언제나 고유해야 합니다.

실제 프로젝트를 예를 들자면, 우리가 데이터베이스에 데이터를 추가하면 주로 해당 데이터를 가르키는 고유 id 가 있습니다. 그러한 데이터를 리액트에서 렌더링하게된다면 그 고유 id 를 가지고 key 로 사용하시면 됩니다.

지금 우리의 경우엔 전화번호 정보에서 id 값을 key 값으로 사용해주었습니다.

이제 PhoneInfoList 컴포넌트를 App 에서 렌더링해주세요. 그리고, data 값을 props 로 전달하세요.

// file: src/App.js
import React, { Component } from 'react';
import PhoneForm from './components/PhoneForm';
import PhoneInfoList from './components/PhoneInfoList';

class App extends Component {
  id = 2
  state = {
    information: [
      {
        id: 0,
        name: '김민준',
        phone: '010-0000-0000'
      },
      {
        id: 1,
        name: '홍길동',
        phone: '010-0000-0001'
      }
    ]
  }
  handleCreate = (data) => {
    const { information } = this.state;
    this.setState({
      information: information.concat({ id: this.id++, ...data })
    })
  }
  render() {
    return (
      <div>
        <PhoneForm
          onCreate={this.handleCreate}
        />
        <PhoneInfoList data={this.state.information}/>
      </div>
    );
  }
}

export default App;

코드를 저장하고, 전화번호 정보들이 잘 나타나는지 확인하세요. 그리고, 새 데이터도 등록해보세요.

잘 작동했나요?

가끔씩은, 데이터에 고유 값이 없을 수 도 있습니다. 그럴 때에는 만약에 key 값을 빼먹으면 렌더링이 되긴 하지만 개발자도구 콘솔에서 경고창이 뜨게 됩니다. 만약에 그 경고가 보고싶지 않다면 다음과 같이 작업 할 수 있습니다.

    const list = data.map(
      (info, index) => (<PhoneInfo key={index} info={info}/>)
    );

하지만 기억하세요. 위처럼 하면 단순히 경고만 감출 뿐이고 성능상으로는 key 가 없는 것과 동일합니다.

정리

우리는 데이터를 배열에 어떻게 등록을 해야 할 지, 그리고 등록한 데이터를 어떻게 보여줄 지 배워보았습니다. 이번 튜토리얼에서 여러분이 꼭 기억해야 할 점은 배열을 렌더링 하게 될 때에는 꼭 고유값을 key 로 사용해야 한다는 것 입니다. 그리고, 불변성 유지를 위하여 데이터를 조작 할 때에는 기존의 배열을 건들이지 않는 방식으로 해야 하는데, 이 불변성 유지에 대해서는 나중에 더 자세히 알아보겠습니다.

다음 튜토리얼에서는, 우리가 만든 전화번호부에 있는 데이터를 제거도 하고, 수정도 하는 방법을 알아보겠습니다.

  • DaekyuLee

    PhoneInfo 컴포넌트 div에 style 지정이 안된 코드가 올라와 있는것 같아요.

    • 확인했습니다 🙂 감사합니다!

  • Gradler Kim

    style 선언하신거 적용하려면
    PhoneInfo render함수 내에서 return을 이렇게 수정해야 합니다.
    return (

    {name}
    {phone}

    );

  • Hochan Jo

    강의 감사히 잘 보고 있습니다.
    a.forEach 인데 b로 표기된 것이 있네요.

  • Dani Choi

    동영상에서는 immutability helper를 사용한 방법을 가르쳐 주셨었는데 여기서 사용한 방법과의 차이가 어떤게 있을 까요?

  • sh jin

    오타가 하나 있는 것 같아요 forEach 예를 들어준 구문에
    b.forEach(number => b.push(number * 2));
    -> a.forEach(number => b.push(number * 2));

    정리해주신글들 보고 많이 배워 갑니다. 감사합니다.

  • 이민수

    주소창에 http://localhost:3000/?name='이름'&phone='번호‘ 형식으로 들어가긴 하는데 html에 표시가 안되네요 ㅠㅠ
    오타가 있나 생각해서 복붙을 해보았지만 똑같았습니다…
    왜 이런 현상이 일어날까요?
    console.log도 보여주질 않구요..

  • 정동건

    데이터 추가 부분에서
    id: 0,
    name: ‘이름’,s
    phone: ‘010-0000-0000’

    부분에서 s는 오타 인 것 같습니다.

  • 아운

    감사합니다. 그런데 PhoneInfo에서 static defaultProps 값이 없어도 아무 문제 없이 실행되는것 같습니다… 그리고 디폴트 값이면 아무것도 입력하지 않고 저장시 여기에서 입력한 값으로 저장되어야하는게 아닌가요?

    • donghyeok jang

      PhoneInfoList.js에서 info => () 이부분에서
      info => () 이런식으로 info를 빼먹게 되면 defaultProps에 있는 내용으로 들어가죠

      • manthosi
      • ejlim

        이런경우가 언제 있는걸까요 ??? 코드를 빼먹었을 경우일까요 ????

      • Minjoo Kim

        info={info} 이부분은 어떤걸 의미하는지 모르겠어요. phoneList 컴포넌트에서 {info}는 선언되지 않았었는데 어떻게 쓰인건가요?

  • 이석원

    어잇 컴포넌트 많이 지니까 복잡하네

  • JEong

    하찮은 질문일수도 있겠지만.. concat 부분에서 …data 는 뭘 의미하는건지…알려주실수 있으실까요

    • Choong Mo Good

      information 객체에 id: this.id++ 해서 추가하고 나머지 부분은 매개변수로 받아온 data를 그대로 넣겠다는 의미입니다.

    • 강덕철

      https://sovovy.tistory.com/34

      참고하시면 좋을 것 같습니다.

  • PAPICO

    위에서 네번째 줄 리액트애서는(x) -> 리액트에서는(o) 오타있습니다.

  • Hyunwoo Je

    해당 예제 실행 시, 제가 겪었던 에러를 혹시나 다른 분들이 겪으실수도 있다고 생각하여 공유합니다.
    상기 코드를 그대로 실행하면 다음과 같은 에러가 확인 되었습니다.

    Warning: is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.
    (오해하실수 있지만…. `PhoneInfo`는 정상적으로 CamelCase로 error message가 출력되는데, 여기 reply에 등록을 하면 lowercase로 변환되어 버리네요….)

    해당 에러에 대해 여기저기 구글링 해보았지만… 뾰족한 방법이 없었고,
    혹시, `PhoneInfo`와 `PhoneInfoList` 두 component에 대해서 어떤것을 import 해야하는지 혼동이 있어 발생하는게 아닌가 하는 생각에,

    src/components/PhoneInfoList.js 코드의 import 부분을 다음과 같이 수정하였습니다.
    import PhoneInfo from './components/PhoneInfo.js'

    수정하여 재 기동하니 정상적으로 페이지가 렌더링 되는것을 확인하였습니다.

  • 작성중에 오타가 난것 같습니다.
    의도하신 결과대로라면 이 부분 수정 필요할 것 같아요 ㅎㅎ

    –작성된 내용——————–
    const a = [1,2,3,4,5];
    const b = [];
    b.forEach(number => b.push(number * 2));

    –수정한 내용——————–

    const a = [1,2,3,4,5];
    const b = [];
    a.forEach(number => b.push(number * 2));

    올려주신 글들 보면서 공부하는데 많은 도움이 되었습니다.
    감사합니다.

  • recordboy

    좋은 강의 감사합니다

  • CodeDiver

    React가 이렇게 어려운건가요?
    설명이 너무 어렵습니다.

  • z쿳쿳z

    안녕하세요. 좋은 글 잘보고 있습니다.
    조금 이해가 되지 않는 부분이 있어서 질문드립니다.
    데이터를 추가할 때 id 값을 2로 할당하셨는데…handleCreate에서 데이터를 추가를 하면 id 값이 3이 될거 같지만 실제로 코드로 구현해보니 2부터 id가 추가 되는것을 확인 했습니다. 이유를 아무리 생각해도 잘 모르겠습니다.ㅠㅠ

    • Banya Kim

      증감연산자에 대한 내용인 것 같네요.
      ++a; //선증가 후연산 => 연산자인 ++가 변수의 앞에 있는 경우 a의 값을 1 더한 후 출력
      a++; //후증가 선연산 => 연산자인 ++가 변수의 뒤에 있는 경우 a의 값을 출력 후 1을 더함

      위의 코드의 id++ 는 후증가 선연산이기 때문에, 먼저 id = 2로 연산이 되고 그다음에 증가해서 3이 됩니다.
      만약 코드를 ++id (선증가 후연산)로 바꿔보시면 id값이 3부터 찍히는걸 확인하실 수 있을거에요 🙂

      • z쿳쿳z

        답변 감사합니다. 덕분에 해결 되었습니다~~