7b. 투두리스트 기능 구현하기

App 에 state 추가하기

App.js

import React, { Component } from 'react';
import CreateForm from './components/CreateForm';
import TodoList from './components/TodoList';

import './App.css';

class App extends Component {
  id = 3;

  // state 의 초깃값을 설정합니다.
  state = {
    // 그 초깃값은 배열 형태로 넣어주었고, 내부에 기본 값들을 넣어주었습니다.
    todos: [
      {
        id: 0,
        text: '앵귤러 배우고',
        checked: true
      },
      {
        id: 1,
        text: '리액트 배우고',
        checked: false
      },
      {
        id: 2,
        text: '뷰 배우자',
        checked: false
      }
    ]
  };

  render() {
    return (
      <div className="App">
        <div className="header">
          <h1>오늘 뭐할까?</h1>
        </div>
        <CreateForm />
        <div className="white-box">
          <TodoList todos={this.state.todos} />
        </div>
      </div>
    );
  }
}

export default App;

초기데이터 지정 후 TodoList 로 이 todos 값을 전달합니다.

자바스크립트 배열의 내장함수 몇가지 배워보기

계속해서 진행하기전에 자바스크립트 배열의 내장함수 몇가지를 배워보겠습니다.

concat

concat 은 배열에 특정 값, 혹은 또다른 배열을 붙여주는 내장함수입니다. 그리고, 기존의 배열은 건드리지 않습니다.

const array = [0, 1, 2];
const after = array.concat(3); // 결과 [0, 1, 2, 3]
const stick = array.concat([4, 5, 6]); // 결과 [0, 1, 2, 4, 5, 6]

map

기존에 있는 배열을 가지고 특정 로직을 통하여 새로운 배열을 만들어 낼 때 사용합니다.

const array = [0, 1, 2, 3, 4, 5];
function square(number) {
  return number * number;
}

const squared = array.map(square);
// 결과: [0, 2, 4, 9, 16, 25]

map 에서 사용 할 함수를 따로 선언하지 않고, map 내부에 작성 할 수도 있죠.

const array = [0, 1, 2, 3, 4, 5];
const squared = array.map(n => n * n);

map 함수의 핵심은, 기존의 배열은 건들이지 않는다는 것 입니다.

리액트에서는 이 map 함수가 주로 두가지 용도로 사용됩니다.

(1) 컴포넌트 배열 렌더링

첫번째는 배열의 내용을 렌더링 해야 할 때입니다. 우리가 방금 숫자를 제곱한 것 처럼, 데이터들로 이뤄진 배열을 가지고 JSX 의 배열로 변환 할 수 있습니다.

const ShowNumbers = () => {
  const array = [0, 1, 2, 3, 4, 5];
  const numberList = array.map(n => <div key={n}>{n}</div>)
  return (
    <div>{numberList}</div>
  )
}

컴포넌트를 배열형태로 렌더링할 때에는, key 값을 사용해야하는데, 이 값은 고유해야합니다. 리액트에서는 배열 형태의 컴포넌트들을 렌더링하고, 내부의 정보가 변경되거나, 새로 추가될 시에 이 key 값을 참고하여 효율적으로 변화를 일으킵니다.

주로 이 key 값은, 고유해야 하기에 데이터베이스에서 전달되는 고유 ID 로 설정됩니다. 만약에 데이터베이스가 따로 없는 경우엔, 데이터를 등록 할 때마다 고유한 ID 값을 직접 집어넣어주어야 하고, 고유한 ID 가 없으면 기본적으로는 그냥 배열의 순서값 (index) 가 key 로 설정됩니다.

만약에 key 를 설정하지 않으면 리액트에서 경고를 띄우게 되는데 이 경고가 보고싶지 않다면 직접 index 값을 key 로 넣어주면 되긴합니다. map 함수에서 두번째 index 값이여서, 이렇게 할 수 있습니다.

const numberList = array.map((n, i) => <div key={i}>{n}</div>)

그런데 결코 좋은 방법은 아니므로 왠만하면 피해야합니다 (경고만 안뜰뿐 key 값을 설정하지 않은 것과 똑같은 성능입니다.)

key 가 없을 때

key 가 있을 때

(2) 불변성 지키면서 데이터 변환

두번째로는, 리액트에서 컴포넌트에서 사용하는 상태를 업데이트 해주려면 기존의 데이터를 건들이지 않고 새로 만들어주는 방식으로 값을 설정해주어야합니다.

따라서, 만약에 배열에 있는 특정 데이터를 바꾸고 싶을땐 이렇게 할 수 있습니다.

const data = [
  {
    id: 0,
    value: true
  },
  {
    id: 1,
    value: false
  }
];
const nextData = data.map(
  o => (o.id === 1)
    ? { ...o, value: !o.value }
    : o
);

이렇게 하면 기존 데이터를 수정하지 않으면서 새 값을 지니고있는 새 데이터를 만들 수 있죠.

불변성을 지키는 이유는, shouldComponentUpdate 로 필요할 때 성능 최적화를 할 수 있게 하기 위함입니다. 이에 관해서는 여기 에서 더욱 자세히 볼 수 있답니다.

filter

filter 함수는 데이터 배열에서 특정 조건을 만족하는 원소들만 골라서 새 배열을 만들어줄 때 사용합니다.

const numbers = [0, 1, 2, 3, 4, 5];
const gtThree = numbers.filter(n => n > 3);
// 결과: [4, 5]

이것을 응용해서, 기존의 데이터는 그대로 두면서 특정 원소를 제거시킨 배열을 새로 만들어 낼 수도 있습니다. 예를들어서 위 배열에서 3을 없애고 싶다면 이렇게 할 수 있죠.

const numbers = [0, 1, 2, 3, 4, 5];
const withoutThree = numbers.filter(n => n !== 3);
// 결과: [0, 1, 2, 4, 5]

이는 불변성을 지키며 데이터를 제거시켜야 할 때 유용하게 사용 할 수 있습니다.

TodoList 에서 배열 map 하기

데이터 배열을 컴포넌트로 map 하여 렌더링 해봅시다!

components/TodoList.js

import React from 'react';
import TodoItem from './TodoItem';

const TodoList = ({ todos }) => {
  const todoList = todos.map(todo => (
    <TodoItem
      id={todo.id}
      key={todo.id}
      checked={todo.checked}
      text={todo.text}
    />
  ));
  // 배열에 key 가 설정되어있다면
  // 배열을 그대로 렌더링 할 수도 있습니다 - 리액트 16 기준
  return todoList;
};

export default TodoList;

TodoItem 에서 데이터 정보 반영하기

방금 전달한 checked, text 값을 TodoItem 컴포넌트에서 반영합시다.

import React from 'react';
import './TodoItem.css';

const TodoItem = ({ checked, text, id }) => (
  <div className={`TodoItem ${checked && 'active'}`}>
    <div className="check">&#10004;</div>
    <div className="text">{text}</div>
    <div className="remove">[지우기]</div>
  </div>
);

export default TodoItem;

여기까지 하면 다음과 같이 App 에서의 state 가 TodoItem 까지 전달 될 것입니다.

데이터 추가기능 구현하기

우리가 이전에 배운 concat 함수를 사용해서 배열에 새 데이터를 집어넣고 이를 setState 를 통하여 설정하겠습니다. handleCreate 를 다음과 같이 구현하세요. 구현 이후엔, CreateForm 컴포넌트에 onSubmit 값으로 전달해줍니다.

App.js

import React, { Component } from 'react';
import CreateForm from './components/CreateForm';
import TodoList from './components/TodoList';

import './App.css';

class App extends Component {
  id = 3;

  // state 의 초깃값을 설정합니다.
  state = {
    // 그 초깃값은 배열 형태로 넣어주었고, 내부에 기본 값들을 넣어주었습니다.
    todos: [
      {
        id: 0,
        text: '앵귤러 배우고',
        checked: true
      },
      {
        id: 1,
        text: '리액트 배우고',
        checked: false
      },
      {
        id: 2,
        text: '뷰 배우자',
        checked: false
      }
    ]
  };

  handleCreate = text => {
    // 데이터 만들고
    const todoData = {
      id: this.id++,
      text,
      checked: false
    };
    // 데이터를 등록
    this.setState({
      todos: this.state.todos.concat(todoData)
    });
  };

  render() {
    return (
      <div className="App">
        <div className="header">
          <h1>오늘 뭐할까?</h1>
        </div>
        <CreateForm onSubmit={this.handleCreate} />
        <div className="white-box">
          <TodoList todos={this.state.todos} />
        </div>
      </div>
    );
  }
}

export default App;

그 다음에는, CreateForm 컴포넌트에서 state 에 input 의 상태를 관리하도록 하고, 버튼이 눌리면 props 로 받은 onSubmit 에 현재 input 값을 넣어서 호출해줍니다.

components/CreateForm.js

import React, { Component } from 'react';
import './CreateForm.css';

class CreateForm extends Component {
  state = {
    input: ''
  };

  handleChange = e => {
    this.setState({
      // 앞으로 바뀔 input 의 값은 e.target.value 에 있습니다.
      input: e.target.value
    });
  };

  handleSubmit = e => {
    // Form Submit 은 페이지를 새로고침을 트리거하는데
    // 이를 방지해줍니다.
    e.preventDefault();
    this.props.onSubmit(this.state.input);
    this.setState({ input: '' });
  };

  render() {
    return (
      <form className="CreateForm" onSubmit={this.handleSubmit}>
        <input placeholder="오늘 뭐하지..?" onChange={this.handleChange} value={this.state.input}/>
        <button type="submit">추가</button>
      </form>
    );
  }
}

export default CreateForm;

이제, 새 데이터를 추가 할 수 있게 될 것입니다.

투두 토글하기

각 투두 데이터의 체크 값을 껐다 키는 기능을 구현하겠습니다. 이걸 구현하기 위해서는, map 을 통해서 원하는 값을 바꾼 새 배열을 만든 다음에 이 값을 setState 로 설정하면 됩니다.

handleCreate 를 다음과 같이 구현하세요, 그리고 그 handleCreate 를 TodoList 로 onCheck 라는 props 로 전달하세요.

App.js

import React, { Component } from 'react';
import CreateForm from './components/CreateForm';
import TodoList from './components/TodoList';

import './App.css';

class App extends Component {
  id = 3;

  // state 의 초깃값을 설정합니다.
  state = {
    // 그 초깃값은 배열 형태로 넣어주었고, 내부에 기본 값들을 넣어주었습니다.
    todos: [
      {
        id: 0,
        text: '앵귤러 배우고',
        checked: true
      },
      {
        id: 1,
        text: '리액트 배우고',
        checked: false
      },
      {
        id: 2,
        text: '뷰 배우자',
        checked: false
      }
    ]
  };

  handleCreate = text => {
    // 데이터 만들고
    const todoData = {
      id: this.id++,
      text,
      checked: false
    };
    // 데이터를 등록
    this.setState({
      todos: this.state.todos.concat(todoData)
    });
  };

  handleCheck = id => {
    // map 을 통하여 원하는 데이터만 바꿔줍니다.
    const nextTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return { ...todo, checked: !todo.checked };
      }
      return todo;
    });
    this.setState({
      todos: nextTodos
    });
  };

  render() {
    return (
      <div className="App">
        <div className="header">
          <h1>오늘 뭐할까?</h1>
        </div>
        <CreateForm onSubmit={this.handleCreate} />
        <div className="white-box">
          <TodoList todos={this.state.todos} onCheck={this.handleCheck} />
        </div>
      </div>
    );
  }
}

export default App;

TodoList 에서는 방금 전달받은 onCheck 을 TodoItem 으로 그대로 전달하세요.

components/TodoList.js

import React from 'react';
import TodoItem from './TodoItem';

const TodoList = ({ todos, onCheck }) => {
  const todoList = todos.map(todo => (
    <TodoItem
      id={todo.id}
      key={todo.id}
      checked={todo.checked}
      text={todo.text}
      onCheck={onCheck}
    />
  ));
  // 배열에 key 가 설정되어있다면
  // 배열을 그대로 렌더링 할 수도 있습니다 - 리액트 16 기준
  return todoList;
};

export default TodoList;

TodoItem 에서는 최상위 DOM 에 onClick 이벤트로 onCheck 을 넣어줍니다. 이 과정에서 () => onCheck(id) 이런식으로 입력을 하게 되는데요, 이렇게 함으로서, 전달받은 함수에 id 값을 파라미터로 넣어서 호출하게 됩니다. 여기서 그냥 onClick={onCheck(id) 라고 작성하면 안됩니다. 그렇게 하면, 렌더링 될 때마다 이 함수가 호출되면서 리액트 앱이 크래쉬됩니다.

components/TodoItem.js

import React from 'react';
import './TodoItem.css';

const TodoItem = ({ checked, text, id, onCheck }) => (
  <div
    className={`TodoItem ${checked && 'active'}`}
    onClick={() => onCheck(id)}
  >
    <div className="check">&#10004;</div>
    <div className="text">{text}</div>
    <div className="remove">[지우기]</div>
  </div>
);

export default TodoItem;

각 TodoItem 눌러서 체크가 껐다 켜지는지 확인해보세요~

투두 지우기

마지막으로, 우리가 등록한 투두를 지우는 방법을 알아보겠습니다. 배열에서 특정 데이터를 지울 땐 filter 를 사용해서 지우면 편합니다. handleRemove 를 구현하고, 우리가 방금 체크 기능을 구현한것처럼 TodoItem 쪽 까지 props 를 쭉 전달해주세요.

App.js

import React, { Component } from 'react';
import CreateForm from './components/CreateForm';
import TodoList from './components/TodoList';

import './App.css';

class App extends Component {
  id = 3;

  // state 의 초깃값을 설정합니다.
  state = {
    // 그 초깃값은 배열 형태로 넣어주었고, 내부에 기본 값들을 넣어주었습니다.
    todos: [
      {
        id: 0,
        text: '앵귤러 배우고',
        checked: true
      },
      {
        id: 1,
        text: '리액트 배우고',
        checked: false
      },
      {
        id: 2,
        text: '뷰 배우자',
        checked: false
      }
    ]
  };

  handleCreate = text => {
    // 데이터 만들고
    const todoData = {
      id: this.id++,
      text,
      checked: false
    };
    // 데이터를 등록
    this.setState({
      todos: this.state.todos.concat(todoData)
    });
  };

  handleCheck = id => {
    // map 을 통하여 원하는 데이터만 바꿔줍니다.
    const nextTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return { ...todo, checked: !todo.checked };
      }
      return todo;
    });
    this.setState({
      todos: nextTodos
    });
  };

  handleRemove = id => {
    // filter 를 통하여 불필요한 데이터는 필터링합니다.
    const nextTodos = this.state.todos.filter(todo => todo.id !== id);
    this.setState({
      todos: nextTodos
    });
  };

  render() {
    return (
      <div className="App">
        <div className="header">
          <h1>오늘 뭐할까?</h1>
        </div>
        <CreateForm onSubmit={this.handleCreate} />
        <div className="white-box">
          <TodoList
            todos={this.state.todos}
            onCheck={this.handleCheck}
            onRemove={this.handleRemove}
          />
        </div>
      </div>
    );
  }
}

export default App;

components/TodoList.js

import React from 'react';
import TodoItem from './TodoItem';

const TodoList = ({ todos, onCheck, onRemove }) => {
  const todoList = todos.map(todo => (
    <TodoItem
      id={todo.id}
      key={todo.id}
      checked={todo.checked}
      text={todo.text}
      onCheck={onCheck}
      onRemove={onRemove}
    />
  ));
  // 배열에 key 가 설정되어있다면
  // 배열을 그대로 렌더링 할 수도 있습니다 - 리액트 16 기준
  return todoList;
};

export default TodoList;

이제 지우기가 눌려지면 onRemove 가 호출되게 할 것인데요, 지우기가 눌려졌을 때, 최상위 DOM 에 적용되어있는 onClick 이벤트도 함께 발생하게 됩니다. 때문에, 지우기가 눌렸을때는, 클릭 이벤트가 그 위까지 전달되는것을 방지하도록 e.stopPropagation(); 를 호출하겠습니다.

components/TodoItem.js

import React from 'react';
import './TodoItem.css';

const TodoItem = ({ checked, text, id, onCheck, onRemove }) => (
  <div
    className={`TodoItem ${checked && 'active'}`}
    onClick={() => onCheck(id)}
  >
    <div className="check">&#10004;</div>
    <div className="text">{text}</div>
    <div
      className="remove"
      onClick={e => {
        e.stopPropagation();
        onRemove(id);
      }}
    >
      [지우기]
    </div>
  </div>
);

export default TodoItem;

투두 리스트를 완성했습니다!

Edit Todo List (Completed Features)

handleToggle 과 handleRemove 의 경우 다음과 같은 형식으로도 작성 할 수 있습니다.

  handleCheck = id => {
    // 이렇게 작성 할 수도 있음
    this.setState(({ todos }) => ({
      todos: todos.map(
        todo => (todo.id === id ? { ...todo, checked: !todo.checked } : todo)
      )
    }));
  };

  handleRemove = id => {
    // 이것도 이렇게 할 수 있음..
    this.setState(({ todos }) => ({
      todos: todos.filter(todo => todo.id !== id)
    }));
  };

handleCheck 을 이렇게도 작성 할 수 있습니다.

  handleCheck = id => {
    // map 을 통하여 원하는 데이터만 바꿔줍니다.
    const { todos } = this.state; // todos 를 자주 조회할거니까 비구조화 할당으로 레퍼런스 미리 만들고
    const nextTodos = todos.slice(); // slice 를 통하여 배열을 복사하고
    const index = todos.findIndex(todo => todo.id === id);
    const todo = todos[index]; // 우리가 업데이트하고 싶은 todo
    // index 번째 아이템을 수정하고
    nextTodos[index] = {
      ...todo,
      checked: !todo.checked
    };
    this.setState({
      todos: nextTodos // 이를 state 에 집어넣기
    });
  };

조금 길긴하죠? 핵심은 기존의 객체나 배열은 건들이지 않고 새로운 배열이나 객체를 만들어서 교체해주는 것 입니다.

이러한 작업이 귀찮을 수도 있긴한데, 당연하게도, 더 편하게 하는 방법들은 우리가 차차 배워나갈 것 입니다. (사실 위 방식들도 익숙해지면 정말 간단한 작업들이긴 합니다.) 그리고, 이렇게 불변성 유지를 해서 얻을 수 있는 이점은, 컴포넌트 업데이트 성능을 더욱 쉽게 최적화 할 수 있다는 점 입니다.

results matching ""

    No results matching ""