누구든지 하는 리액트 8편: 배열 다루기 (2) 제거와 수정


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

우리는 지난 섹션에서 배열에 데이터를 추가하는 방법과 배열 내부의 내용들을 화면에 보여주는 기능을 구현해보았습니다. 이번에는, 배열 내부의 데이터를 제거하는 방법과 수정을 하는 방법을 알아보겠습니다.

일단 제거부터 시작해볼까요?

데이터 제거

기존의 배열 데이터를 건들이지 않으면서 데이터를 제거하기 위해선, 여러가지 방법이 있을 수 있습니다.

컴포넌트에서 직접 구현을 하기 전에, 먼저 예제삼아 일반 자바스크립트 배열을 가지고 연습을 해보겠습니다.

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

배열에서 3을 제거 할건데요, 기존의 배열은 그대로 유지하고 새 배열을 만들어서 3을 제외시키겠습니다.

첫번째 방법은 slice 와 concat 을 이용하는겁니다. 3 왼쪽의 배열과 그 우측의 배열을 서로 합쳐주는 것이죠.

array.slice(0,2).concat(array.slice(3,5)) // [1, 2, 4, 5]

배열 전개 연산자를 사용하면 다음과 같이 구현을 할 수도 있습니다.

[ ...array.slice(0,2), ...array.slice(3,5) ];

하지만 이것보다 훨씬 간단한 방법으로도 구현 할 수 있습니다. 우리는 단순히 값이 3인걸 없애는 것이죠?

배열에는 filter 라는 내장함수가 있는데, 이 함수는 특정 조건에 부합되는 원소들만 뽑아내서 새 배열을 만들어줍니다.

따라서, 3이 제외된 배열을 만들기 위해서 이러한 코드를 작성 할 수도 있지요.

array.filter(num => num !== 3); // [1, 2, 4, 5]

이렇게 하면, 3이 아닌 것들만 필터링을 해서 새 배열을 보여주겠죠?

그럼, 동일한 방식으로, 전화번호정보를 데이터에서 제외시키는 기능을 구현해보겠습니다.

id 를 파라미터로 받아오는 handleRemove 라는 함수를 만드시고, PhoneInfoList 로 전달하세요.

// 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 })
    })
  }
  handleRemove = (id) => {
    const { information } = this.state;
    this.setState({
      information: information.filter(info => info.id !== id)
    })
  }
  render() {
    const { information } = this.state;
    return (
      <div>
        <PhoneForm
          onCreate={this.handleCreate}
        />
        <PhoneInfoList 
          data={information}
          onRemove={this.handleRemove}
        />
      </div>
    );
  }
}

export default App;

PhoneInfoList 에서는 props 로 전달받은 onRemove 를 그대로 전달해주겠습니다. 이 함수가 전달되지 않았을 경우를 대비하여 해당 props 를 위한 defaultProps 도 설정하세요.

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

class PhoneInfoList extends Component {
  static defaultProps = {
    list: [],
    onRemove: () => console.warn('onRemove not defined'),
  }

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

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

export default PhoneInfoList;

그 다음에는, PhoneInfo 쪽에서 삭제 기능을 구현해주겠습니다. 우리는 삭제 버튼을 만들어서 해당 버튼에 이벤트를 설정하겠습니다.

import React, { Component } from 'react';

class PhoneInfo extends Component {
  static defaultProps = {
    info: {
      name: '이름',
      phone: '010-0000-0000',
      id: 0
    },
  }

  handleRemove = () => {
    // 삭제 버튼이 클릭되면 onRemove 에 id 넣어서 호출
    const { info, onRemove } = this.props;
    onRemove(info.id);
  }

  render() {
    const style = {
      border: '1px solid black',
      padding: '8px',
      margin: '8px'
    };

    const {
      name, phone
    } = this.props.info;
    
    return (
      <div style={style}>
        <div><b>{name}</b></div>
        <div>{phone}</div>
        <button onClick={this.handleRemove}>삭제</button>
      </div>
    );
  }
}

export default PhoneInfo;

삭제 버튼을 눌러보세요. 데이터가 제대로 제거 되나요?

데이터 수정

이번엔 데이터 수정을 해보겠습니다. 수정할때도 마찬가지로 불변성을 지켜줘야합니다. 기존의 배열과, 그리고 그 내부에있는 객체를 절대로 직접적으로 수정하시면 안됩니다.

예를 들어서 다음과 같은 객체로 이뤄진 배열이 있다고 가정해봅시다.

const array = [
  { id: 0, text: 'hello', tag: 'a' },
  { id: 1, text: 'world' , tag: 'b' },
  { id: 2, text: 'bye', tag: 'c' }
];

여기서 기존의 값을 건들이지 않고 id 가 1인 객체의 text 값을 ‘Korea’ 라는 값으로 바꾼 새로운 배열을 만들어보겠습니다.

const modifiedArray = array.map(item => item.id === 1
  ? ({ ...item,. text: 'Korea' }) // id 가 일치하면 새 객체를 만들고, 기존의 내용을 집어넣고 원하는 값 덮어쓰기
  : item // 바꿀 필요 없는것들은 그냥 기존 값 사용

그러면 한번, 같은 원리를 사용하여 우리의 전화번호 정보를 수정해보겠습니다.

우리는 handleUpdate 라는 함수를 만들건데요, 이 함수는 id 와 data 라는 파라미터를 받아와서 필요한 정보를 업데이트 해줍니다.
이 handleUpdate 는 PhoneInfoList 의 onUpdate 로 전달해주세요.

// 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 })
    })
  }
  handleRemove = (id) => {
    const { information } = this.state;
    this.setState({
      information: information.filter(info => info.id !== id)
    })
  }
  handleUpdate = (id, data) => {
    const { information } = this.state;
    this.setState({
      information: information.map(
        info => id === info.id
          ? { ...info, ...data } // 새 객체를 만들어서 기존의 값과 전달받은 data 을 덮어씀
          : info // 기존의 값을 그대로 유지
      )
    })
  }
  render() {
    const { information } = this.state;
    return (
      <div>
        <PhoneForm
          onCreate={this.handleCreate}
        />
        <PhoneInfoList 
          data={information}
          onRemove={this.handleRemove}
          onUpdate={this.handleUpdate}
        />
      </div>
    );
  }
}

export default App;

그럼 이제 PhoneInfoList 컴포넌트를 업데이트 해줘야 하겠죠?

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

class PhoneInfoList extends Component {
  static defaultProps = {
    data: [],
    onRemove: () => console.warn('onRemove not defined'),
    onUpdate: () => console.warn('onUpdate not defined'),
  }

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

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

export default PhoneInfoList;

그리고, 데이터를 컴포넌트로 렌더링하는 과정에서 PhoneInfo 에 onUpdate 를 그대로 전달해주었습니다.

그럼 이젠 PhoneInfo 컴포넌트를 업데이트 해줄 차례입니다. 이번엔 수정할 코드는 꽤 많은데요, 주석을 읽어가면서 코드를 작성해주세요.

import React, { Component } from 'react';

class PhoneInfo extends Component {
  static defaultProps = {
    info: {
      name: '이름',
      phone: '010-0000-0000',
      id: 0
    },
  }

  state = {
    // 우리는 수정 버튼을 눌렀을 떄 editing 값을 true 로 설정해줄것입니다.
    // 이 값이 true 일 때에는, 기존에 텍스트 형태로 보여주던 값들을
    // input 형태로 보여주게 됩니다.
    editing: false,
    // input 의 값은 유동적이겠지요? input 값을 담기 위해서 각 필드를 위한 값도
    // 설정합니다
    name: '',
    phone: '',
  }

  handleRemove = () => {
    // 삭제 버튼이 클릭되면 onRemove 에 id 넣어서 호출
    const { info, onRemove } = this.props;
    onRemove(info.id);
  }

  // editing 값을 반전시키는 함수입니다
  // true -> false, false -> true
  handleToggleEdit = () => {
    const { editing } = this.state;
    this.setState({ editing: !editing });
  }

  // input 에서 onChange 이벤트가 발생 될 때
  // 호출되는 함수입니다
  handleChange = (e) => {
    const { name, value } = e.target;
    this.setState({
      [name]: value
    });
  }


  componentDidUpdate(prevProps, prevState) {
    // 여기서는, editing 값이 바뀔 때 처리 할 로직이 적혀있습니다.
    // 수정을 눌렀을땐, 기존의 값이 input에 나타나고,
    // 수정을 적용할땐, input 의 값들을 부모한테 전달해줍니다.

    const { info, onUpdate } = this.props;
    if(!prevState.editing && this.state.editing) {
      // editing 값이 false -> true 로 전환 될 때
      // info 의 값을 state 에 넣어준다
      this.setState({
        name: info.name,
        phone: info.phone
      })
    }

    if (prevState.editing && !this.state.editing) {
      // editing 값이 true -> false 로 전환 될 때
      onUpdate(info.id, {
        name: this.state.name,
        phone: this.state.phone
      });
    }
  }
  
  render() {
    const style = {
      border: '1px solid black',
      padding: '8px',
      margin: '8px'
    };

    const { editing } = this.state;

    
    if (editing) { // 수정모드
      return (
        <div style={style}>
          <div>
            <input
              value={this.state.name}
              name="name"
              placeholder="이름"
              onChange={this.handleChange}
            />
          </div>
          <div>
            <input
              value={this.state.phone}
              name="phone"
              placeholder="전화번호"
              onChange={this.handleChange}
            />
          </div>
          <button onClick={this.handleToggleEdit}>적용</button>
          <button onClick={this.handleRemove}>삭제</button>
        </div>
      );
    }


    // 일반모드
    const {
      name, phone
    } = this.props.info;
    
    return (
      <div style={style}>
        <div><b>{name}</b></div>
        <div>{phone}</div>
        <button onClick={this.handleToggleEdit}>수정</button>
        <button onClick={this.handleRemove}>삭제</button>
      </div>
    );
  }
}

export default PhoneInfo;

코드를 다 작성하셨다면, 결과물이 다음과 같이 나타나는지 확인해보세요.

수정이 잘 되나요?

정리

이제 리액트 state 에 있는 배열 안의 데이터를 삭제하는 방법과, 수정하는 방법도 배워보았습니다. 작업을 하시면서, 왜 배열과 객체 값을 직접 수정하면 안되고 불변성을 유지하면서 데이터를 새로 생성하는 방식으로 교체해줘야 하는지에 궁금하실 수도 있습니다.

그것에 대해선, 다음 섹션에서 이름으로 전화번호를 찾는 기능을 구현하면서, 불변성을 왜 유지해야 하는지 알아보겠습니다.

  • Flower Man

    배열을 추가하거나 편집하거나 삭제할때 이전에 velopert님이 유튜브강좌에서 소개해준 “react-addons-update”을 써왔는데 과연 둘중의 방법중에서 어느것이 더 좋은지 고민이 되는데 답변을 해주시면 감사하겠습니다.

  • 저는 Immer.js >= Immutable.js > Plain JavaScript > react-addons-update 순서로 선호합니다 🙂
    개인적으로 Immer.js 를 가장 좋아합니다. 사용성도 좋고 나중에 타입지원도 깔끔해서요.
    Immutable.js 는 강력합니다. Immer.js 보다 편하지만, Immutable.js 는 타입 지원이 귀찮아요
    react-addons-update 는 제가 이걸 사용했던것을 후회할정도로 별로입니다 ㅋㅋ 성능도 별로고 사용성도 별로고..
    그걸 사용하느니 간단한건 일반 자바스크립트로 해결하는게 저는 좋습니다.

  • Goobong

    좋은 강의 감사합니다.
    데이터 수정 modifiedArray 예제에
    `. text` -> `text`
    마지막에 닫는 괄호가 빠진 것 같습니다 🙂

  • Ji Won Kim

    위 예제 코드 중 첫번째 배열에서 3을 제외하는 코드가 수정되어야 할것 같습니다~.
    arr.slice(0,2).concat(4,5) 이렇게 해야 아래와 같은 결과가 나오는데, 위에 예제에서는 arr.slice(0,2).concat(3,5)로 되어 있네요.
    [ 1, 2, 4, 5 ]

  • Youngho Jo

    안녕하세요, 너무나도 잘 정주행 하고있습니다.
    질문이 있는데요..

    ‘적용’ 버튼을 눌렀을때 editing이 false가 되면서 다시 ‘일반모드’로 렌더링이 되는거죠?
    그런데 여기서 ‘적용’을 눌렀을때 부모로 수정된 props가 전달되는 과정은 componentDidUpdate()에서 발생하는게 아닌가 싶습니다.
    하지만 componentDidUpdate()를 위의 코드에서는 어디서도 호출하고 있지 않은데요.
    어떻게 호출없이 동작하는건지 모르겠습니다…ㅜㅜ

    감사합니다.

    • Youngho Jo

      아하…
      a..p..i.. !!!

      이게 이렇게 쓰이는 줄 몰랐네요
      좋은 포스팅 감사합니다 🙂

  • ? ({ …item,. text: ‘Korea’ })
    이부분에 item뒤의 ,뒤에 .이 추가되는 오타가 있습니다.

  • sudal

    안녕하세요 친절한 튜토리얼 잘 보고 있습니다! 강의 감사합니다.
    한가지 궁금한 것이 있습니다.
    Phoneform.js의 handleSubmit에는 e.preventDefault();를 해주었는데
    왜 수정, 삭제를 할 때는 e.preventDefault를 해주지 않으신건가요?

    • Shamp

      등록을 할때는 onSubmit 이벤트로 함수를 호출하기때문입니다.
      onSubmit 의 기본이벤트를 찾아보시면 도움이 되실듯합니다.
      로 함수를 호출하였느냐 또는 form 안에있는 일반적인 버튼 onClick=” 이벤트로 함수를 호출했느냐 차이입니다.

      onClick과 onSubmit 차이죠

  • Xiahoo

    Only one word – Great!