안 쓰던 블로그

To Do List 웹 프로젝트-상태 관리 본문

Web/React

To Do List 웹 프로젝트-상태 관리

proqk 2020. 7. 23. 03:41
반응형

https://foxtrotin.tistory.com/226 이전 글에서 UI를 만들었는데 아직 상태 관리를 설정하지 않았기 때문에 껍데기만 있다

실제로 프로젝트를 작동시키려면 뷰에서 보여 줄 상태를 관리해야 한다

개발을 할 때 상태 관리는 주로 기능별로 상태가 필요한 컴포넌트들을 감싸는 상위 컴포넌트에서 하는 것이 편리하다

지금 프로젝트처럼 소규모라면 App에서 다 해주면 된다

 

예를 들어 데이터 배열의 상태를 TodoItemList 컴포넌트에서 정의, TodoInput의 상태를 그 내부에서 정의했다고 가정한다

이 경우 새 데이터를 TodoItemList에 넣으려면 로직을 어떻게 작성해야 할까?

 

일단 ref(다른 컴포넌트에 데이터 전달)를 최대한 사용하지 않는 방법이 좋다

ref가 들어가면 꼬이기 시작하기 때문에 꼭 필요할 때만 넣어야 한다

ref를 쓰지 않으려면 컴포넌트끼리 직접 연락하는 게 아니라 컴포넌트의 부모를 통해서 연락하는 방법을 써야 한다

 

TodoItemList나 TodoInput의 부모는 App.js이므로 App.js의 state에서

input값과 데이터 배열을 정의하고, 이를 변화시키는 메서드도 정의한다

그리고 state값과 메서드를 props로 하위 컴포넌트에 전달해서 사용하면 깔끔하다

 

리덕스라고 아예 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트로 구분하여 컴포넌트를 만들 수 있다

 

 

항목 입력

TodoInput컴포넌트 안에 들어갈 input을 구현하려면 과정은 다음과 같다

 

1. App.js의 state에 input값을 정의

2. input 변경 이벤트 처리할 handleChange메서드 만들기

3. TodoInput 컴포넌트의 props로 전달

 

App.js의 state에 input값을 정의&handleChange 메서드 만들기

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoItemList from './TodoItemList';

class App extends Component {
    id=3 //예시로 0,1,2를 넣었기 때문에 3

    state={
        input: '',
        todos: [
            { id: 0, text: '리액트 공부', checked: false},
            { id: 1, text: '리액트 에러 고치기', checked: false},
            { id: 2, text: '안녕', checked: true}
        ]
    }

    handleChange=(e)=>{
        this.setState({input: e.target.value});
    }

    handleCreate=()=>{
        const{input, todos}=this.state;
        this.setState({
            input: '', //인풋을 비운다
            todos: todos.concat({ //concat으로 배열에 추가한다
                id: this.id++,
                text: input,
                checked: false
            })
        });
    }

    handleKeyPress=(e)=>{ //엔터 눌러도 handleCreate 호출
        if(e.key === 'Enter'){
            this.handleCreate();
        }
    }
    
    render() {
        const {input, todos}=this.state;
        const{
            handleChange,
            handleCreate,
            handleKeyPress
        }=this;

        return (
            <PageTemplate>
                <TodoInput 
                onChange={handleChange}
                value={input}
                onChange={handleChange}
                onCreate={handleCreate}
                />
                
                <TodoItemList todos={todos} />
            </PageTemplate>
        );
    }
}

export default App;

위에는 전체 코드

 

    state={
        input: '',
        todos: [
            { id: 0, text: '리액트 공부', checked: false},
            { id: 1, text: '리액트 에러 고치기', checked: false},
            { id: 2, text: '안녕', checked: true}
        ]
    }

state에 todos라는 객체 배열을 만든다

배열 안에는 id, text, checked 이렇게 세 값이 들어 있다

id는 각 데이터에 고윳값을 부여해서 나중에 컴포넌트로 구성된 배열을 렌더링 할 때 key값으로 쓴다

데이터 변경할 때도 id값으로 데이터를 찾는다

text는 일정 내용, checked는 했는지 안 했는지 여부를 나타낸다

 

    handleCreate=()=>{
        const{input, todos}=this.state;
        this.setState({
            input: '', //인풋을 비운다
            todos: todos.concat({ //concat으로 배열에 추가한다
                id: this.id++,
                text: input,
                checked: false
            })
        });
    }

todoitem 배열을 담은 state를 concat으로 붙여준다

push가 아니라 concat을 쓰는 이유는, 원래는 리렌더링을 할 때 배열을 비교해서 변화한 부분만 바꾸는 최적화를 하는데 push로 배열을 만들면 그 과정을 넘어가버리기 때문이다

그래서 concat으로 기존 배열과 새로운 배열을 합쳐서 넣어주는 식으로 업데이트를 한다

(참고: https://foxtrotin.tistory.com/219 )

 

    render() {
        const {input, todos}=this.state;
        const{
            handleChange,
            handleCreate,
            handleKeyPress
        }=this;

여기서도 state와 메서드 사용할 때 비구조화 할당을 했다

이렇게 레퍼런스를 미리 만들어주면 값을 사용할 때마다 this.props이나 this.를 앞에 붙이지 않아도 된다

 

return (
            <PageTemplate>
                <TodoInput 
                onChange={handleChange}
                value={input}
                onChange={handleChange}
                onCreate={handleCreate}
                />
                
                <TodoItemList todos={todos} />
            </PageTemplate>
        );

todos를 정의했으니까 TodoItemList의 props로 전달한다

 

 

TodoInput 컴포넌트의 props로 전달

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

class TodoItemList extends Component {
    render() {
        const { todos, onToggle, onRemove } = this.props;

        const todoList = todos.map(
            ({id, text, checked}) => (
              <TodoItem
                id={id}
                text={text}
                checked={checked}
                onToggle={onToggle}
                onRemove={onRemove}
                key={id}
              />
            )
          );
        
        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoItemList;

todos 배열을 map함수로 TodoItem으로 구성된 컴포넌트 배열로 쪼갠다

 

 

그러면 이제 체크된 항목은 취소줄과 체크 표시가 뜬다

input에 뭔가를 적고 추가를 누르면 할 일이 추가 된다

 

항목 수정

위에서 input에 적은 일정 정보를 todos 배열에 추가하는 기능을 구현했으니까 이제는 수정을 할 수 있어야 한다

TodoItem을 클릭했을 때 체크 박스를 활성화 하거나 비활성화 해야 한다

 

배열 안의 특정 데이터를 수정하려면

1. id로 원하는 데이터를 찾아서

2. slice와 전개 연산자로 기존 배열을 자르고 새 배열로 만들어 업데이트한다(추가할 때 concat한 거랑 같은 이유로)

 

App.js에 위의 과정을 추가한다

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoItemList from './TodoItemList';

class App extends Component {
    id=3 //예시로 0,1,2를 넣었기 때문에 3

    state={
        input: '',
        todos: [
            { id: 0, text: '리액트 공부', checked: false},
            { id: 1, text: '리액트 에러 고치기', checked: false},
            { id: 2, text: '안녕', checked: true}
        ]
    }

    handleChange=(e)=>{
        this.setState({input: e.target.value});
    }

    handleCreate=()=>{
        const{input, todos}=this.state;
        this.setState({
            input: '', //인풋을 비운다
            todos: todos.concat({ //concat으로 배열에 추가한다
                id: this.id++,
                text: input,
                checked: false
            })
        });
    }

    handleKeyPress=(e)=>{ //엔터 눌러도 handleCreate 호출
        if(e.key === 'Enter'){
            this.handleCreate();
        }
    }

    handleToggle=(id)=>{
        const {todos}=this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const nextTodos = { 
            ...todos[index], 
            checked: !todos[index].checked
        };

        this.setState({ //slice로 index 전후 데이터를 복사
            todos: [
                ...todos.slice(0,index),
                nextTodos,
                ...todos.slice(index+1, todos.length)
            ]
        });
    }
    
    render() {
        const {input, todos}=this.state;
        const{
            handleChange,
            handleCreate,
            handleKeyPress
        }=this;

        return (
            <PageTemplate>
                <TodoInput 
                onChange={handleChange}
                value={input}
                onKeyPress={handleKeyPress}
                onCreate={handleCreate}
                />
                
                <TodoItemList todos={todos} onToggle={this.handleToggle}/>
            </PageTemplate>
        );
    }
}

export default App;

전체 코드

 

handleToggle=(id)=>{
        const {todos}=this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const nextTodos = { 
            ...todos[index], 
            checked: !todos[index].checked
        };

        this.setState({ //slice로 index 전후 데이터를 복사
            todos: [
                ...todos.slice(0,index),
                nextTodos,
                ...todos.slice(index+1, todos.length)
            ]
        });
    }

id로 인덱스를 찾고 값을 반전시킨다(활성화를 비활성화로 아님 반대로 해야 하니까)

그리고 concat했던 것처럼 index 전후 배열+변경될 todo객체 해서 새로운 배열을 만들고 그걸 setState에 넣은다

 

이제 체크 안 된 항목 누르면 체크되고, 된 항목을 누르면 체크 해제가 된다

 

 

항목 삭제

항목 수정까지 했으면 삭제는 수정이랑 거의 똑같다

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInput from './TodoInput';
import TodoItemList from './TodoItemList';

class App extends Component {
    id=3 //예시로 0,1,2를 넣었기 때문에 3

    state={
        input: '',
        todos: [
            { id: 0, text: '리액트 공부', checked: false},
            { id: 1, text: '리액트 에러 고치기', checked: false},
            { id: 2, text: '안녕', checked: true}
        ]
    }

    handleChange=(e)=>{
        this.setState({input: e.target.value});
    }

    handleCreate=()=>{
        const{input, todos}=this.state;
        this.setState({
            input: '', //인풋을 비운다
            todos: todos.concat({ //concat으로 배열에 추가한다
                id: this.id++,
                text: input,
                checked: false
            })
        });
    }

    handleKeyPress=(e)=>{ //엔터 눌러도 handleCreate 호출
        if(e.key === 'Enter'){
            this.handleCreate();
        }
    }

    handleToggle=(id)=>{
        const {todos}=this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const nextTodos = { 
            ...todos[index], 
            checked: !todos[index].checked
        };

        this.setState({ //slice로 index 전후 데이터를 복사
            todos: [
                ...todos.slice(0,index),
                nextTodos,
                ...todos.slice(index+1, todos.length)
            ]
        });
    }

    handleRemove=(id)=>{
        const {todos}=this.state;
        const index = todos.findIndex(todo=>todo.id===id);

        this.setState({
            todos: [
                ...todos.slice(0, index),
                ...todos.slice(index+1, todos.length)
            ]
        });
    }
    
    render() {
        const {input, todos}=this.state;
        const{
            handleChange,
            handleCreate,
            handleKeyPress,
            handleRemove
        }=this;

        return (
            <PageTemplate>
                <TodoInput 
                onChange={handleChange}
                value={input}
                onKeyPress={handleKeyPress}
                onCreate={handleCreate}
                />
                
                <TodoItemList 
                todos={todos} 
                onToggle={this.handleToggle}
                onRemove={handleRemove}
                />
            </PageTemplate>
        );
    }
}

export default App;

App.js 전체 코드

 

    handleRemove=(id)=>{
        const {todos}=this.state;
        const index = todos.findIndex(todo=>todo.id===id);

        this.setState({
            todos: [
                ...todos.slice(0, index),
                ...todos.slice(index+1, todos.length)
            ]
        });
    }

수정에서는 id로 항목 찾고 index까지 자르고 객체 넣고 붙였는데

삭제는 그냥 index빼고 새로 배열 만들면 된다

 

render에 handleRemove까지 넣어주면 이제 x누르면 항목이 삭제된다

 

 

 

참고: 리액트를 다루는 기술_김민준 저, https://velopert.com/3480

반응형
Comments