본문 바로가기
JavaScript/React

React) 일정 관리 웹 애플리케이션 만들기 - 2 (todoList 렌더링, 항목 추가 기능 구현하기)

by 박채니 2022. 11. 19.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

* 프로젝트 준비 및 UI 구성은 아래 포스트 참고

https://chanychu.tistory.com/450

 

React) 일정 관리 웹 애플리케이션 만들기 - 1 (프로젝트 준비 및 UI 구성)

안녕하세요, 코린이의 코딩 학습기 채니 입니다. [리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다. 프로젝트 준비하기 프로젝트 생성 및 필요한 라이브러리 설치 $ yarn cr

chanychu.tistory.com

 

기능 구현하기

 

App에서 todos 상태 사용하기

 

추후 추가할 일정 항목에 대한 상태들은 모두 App 컴포넌트에서 관리합니다.

따라서 App에서 useState를 이용해 todos 상태를 정의하고 TodoList의 props로 전달하겠습니다.

 

App.js

import { useState } from 'react';
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate";

const App = () => {
  const [ todos, setTodos ] = useState([
    {
      id: 1,
      text: '리액트의 기초 알아보기',
      checked: true
    },
    {
      id: 2,
      text: '컴포넌트 스타일링 해보기',
      checked: true
    },
    {
      id: 3,
      text: '일정 관리 앱 만들어보기',
      checked: false
    }
  ]);

  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList todos={todos} />
    </TodoTemplate>
  )
}

export default App;

생성한 todosid, text, check여부를 가진 객체를 가지고 있는 배열이고, TodoList에 props로 전달하였습니다.

TodoList에서 이를 받아 다시 TodoItem에게 각 todo들을 props로 전달해주어야겠죠.

 

TodoList.js

import './TodoList.scss';
import TodoListItem from './TodoListItem';

const TodoList = ({ todos }) => {
    return (
        <div className='TodoList'>
            {
                todos.map(todo => (
                    <TodoListItem todo={todo} key={todo.id} />
                ))
            }
        </div>
    )
}

export default TodoList;

props로 todos를 받아 map을 이용하여 TodoListItem으로 이루어진 배열로 변환하여 렌더링 해줍니다.

map을 사용해 컴포넌트 변환 시, 반드시 key props를 전달해줘야 하므로 todo의 id값을 key값으로 설정해주었습니다.

이제 TodoListItem에서 넘겨 받은 todo를 이용해 렌더링 시켜주겠습니다.

단, check 여부에 따라 보여지는 UI가 달라야하므로 classnames를 이용해 조건부 스타일링을 적용하겠습니다.

 

※ 이 때, 저는 map() 함수 이용 시 이전에는 todos.map((todo) => { ... }) 로 사용하였는데, 현재 코드를 보면 중괄호 { ... }가 아닌 괄호 ( ... )로 감싸는 것을 확인하였습니다.

의문이 들었고, 구글링을 해보니 객체를 반환할 시 괄호 ()로 감싸주어야 한다는 정보를 얻었습니다.

추후 헷갈릴 수 있으니 꼭 기억하기!

https://www.codeit.kr/community/threads/35964

 

여기서는 왜 map 안에 화살표 함수에서 {}로 안 감싸고 ()로 감싸나요 | 코드잇

function FoodList({ items }) { return ( <ul> {items.map((item) => ( <li> <FoodListItem item={item} /> </li> ))} </ul> ); } 라고 하셨는데 왜 아래가 아닌지 궁금합니다. 바뀐곳에 느낌표를 앞뒤로 붙였어요 function FoodList({ ite

www.codeit.kr

 

TodoListItem.js

import cn from 'classnames'
import { MdCheckBox, MdRemoveCircleOutline, MdCheckBoxOutlineBlank } from 'react-icons/md';
import './TodoListItem.scss';

const TodoListItem = ({ todo }) => {
    const { text, checked } = todo;

    return (
        <div className='TodoListItem'>
            <div className={cn('checkbox', { checked })}>
                {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
                <div className='text'>{text}</div>
            </div>
            <div className='remove'>
                <MdRemoveCircleOutline />
            </div>
        </div>
    );
}

export default TodoListItem;

넘겨받은 todo 값에 따라 UI를 설정해주었습니다.

이 때 classnames를 이용하여 'checkbox' 클래스checked가 true라면 'checked' 클래스를 적용하게 해주었습니다.

또한, checked 여부에 따라 체크박스의 icon을 달리 표현해주었습니다.

 


항목 추가 기능 구현하기

 

TodoInsert 컴포넌트에서 인풋 상태를 관리하고, App 컴포넌트에는 todos 배열에 추가하는 함수를 생성해줘야 합니다.

 

TodoInsert value 상태 관리하기

 

TodoInsert.js

import { MdAdd } from 'react-icons/md';
import { useState, useCallback } from 'react';
import './TodoInsert.scss';

const TodoInsert = () => {
    // 인풋 입력 값 상태
    const [ value, setValue ] = useState('');
    
    const onChange = useCallback(e => {
        setValue(e.target.value);
    }, []); // 처음 렌더링될 때만 함수 생성

    return (
        <form className='TodoInsert'>
            <input placeholder="할 일을 입력하세요" value={value} onChange={onChange}/>
            <button type="submit">
                <MdAdd />
            </button>
        </form>
    )
}

export default TodoInsert;

사용자가 인풋에 입력한 값을 관리하기 위해 value로 상태를 지정해주었습니다.

또한, onChange 이벤트 발생 시 value값을 set해주기 위해 setValue해주는 onChange 함수를 생성하였습니다.

이 때 리렌더링 시 함수를 다시 만들 필요가 없으므로, 렌더링될 때만 함수를 생성하고 재사용할 수 있는 useCallback Hook을 이용해 setValue 해주었습니다.

 

인풋에 값을 입력하여 state가 잘 업데이트되는 지 확인하고 싶은데 console.log가 아닌 리액트 개발자 도구를 사용해 확인해보겠습니다.

 

리액트 개발자 도구

 

크롭 웹 스토어 주소

https://chrome.google.com/webstore/category/extensions?hl=ko& 

 

Chrome 웹 스토어

Chrome에 사용할 유용한 앱, 게임, 확장 프로그램 및 테마를 찾아보세요.

chrome.google.com

 

'React Developer Tools'를 검색하여 설치해줍니다.

그 후 개발자 도구를 열면 도구 탭에 Components가 나타나게 됩니다.

확인하고자하는 컴포넌트를 클릭 후 (TodoInsert) 인풋을 수정하면 Hooks의 State 부분에도 반영되는 것을 확인할 수 있습니다.

 

 

todos 배열에 새 객체 추가하기

 

App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만들겠습니다.

새로운 객체 생성 시, id + 1 값을 부여해줘야하므로 이는 useRef를 이용해보겠습니다.

※ useState가 아닌 useRef를 사용하는 이유는?

id 값은 렌더링되는 정보가 아니기 때문입니다.

예를 들어, 해당 값은 화면에 보이지도 않고 값이 변경된다고 해도 리렌더링될 필요도 없습니다. 단순 참조값므로!

 

이 또한 컴포넌트 성능을 아끼기 위해 useCallback을 이용해주겠습니다.

☆ props로 전달해야 할 함수 생성 시 useCallback을 사용해 함수를 감싸주는 것을 습관화!

 

App.js

import { useState, useRef, useCallback } from 'react';
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate";

const App = () => {
  const [ todos, setTodos ] = useState([
    {
      id: 1,
      text: '리액트의 기초 알아보기',
      checked: true
    },
    {
      id: 2,
      text: '컴포넌트 스타일링 해보기',
      checked: true
    },
    {
      id: 3,
      text: '일정 관리 앱 만들어보기',
      checked: false
    }
  ]);

  // 고윳값으로 사용될 id
  // ref를 사용해 변수 담기
  const nextId = useRef(4);

  const onInsert = useCallback(text => {
    const todo = {
      id: nextId.current,
      text,
      checked: false
    };

    setTodos(todos.concat(todo));
    nextId.current += 1; // nextId 1씩 더해줌
  }, [todos]);

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert} />
      <TodoList todos={todos} />
    </TodoTemplate>
  )
}

export default App;

useRef를 이용해 변수를 담아주었습니다.

onInserttodos가 변경될 때만 리렌더링 시켜주도록 하였고, concat을 이용해 생성된 todo를 추가한 배열을 생성하여 setTodos 해줍니다.

또한 다음 추가될 todo의 아이디 값을 위해 nextId를 +1 시켜주었고, 해당 함수를 TodoInsert의 props로 넘겨주었습니다.

 

TodoInsert에서 onSubmit 이벤트 설정하기

 

TodoInsert.js

import { MdAdd } from 'react-icons/md';
import { useState, useCallback } from 'react';
import './TodoInsert.scss';

const TodoInsert = ({ onInsert }) => {
    // 인풋 입력 값 상태
    const [ value, setValue ] = useState('');
    
    const onChange = useCallback(e => {
        setValue(e.target.value);
    }, []); // 처음 렌더링될 때만 함수 생성

    const onSubmit = useCallback(e => {
        onInsert(value); // text 파라미터로 넘겨줌
        setValue(''); // value 값 초기화
        
        // submit 이벤트는 브라우저에서 새로고침을 발생하므로, 이를 방지해줌
        e.preventDefault();
    }, [onInsert, value]);

    return (
        <form className='TodoInsert' onSubmit={onSubmit}>
            <input placeholder="할 일을 입력하세요" value={value} onChange={onChange}/>
            <button type="submit">
                <MdAdd />
            </button>
        </form>
    )
}

export default TodoInsert;

form의 onSubmit 이벤트가 발생할 때 onSubmit 함수가 실행되도록 하였습니다.

해당 함수가 호출되면, props로 받아온 onInsert 함수에 현재 value(text 값)을 파라미터로 넣어 호출해주고, 현재 value값을 초기화해주었습니다.

또한, onSubmit 이벤트는 브라우저를 새로고침하므로 이를 방지하기 위해 preventDefault() 함수를 호출해주었습니다.

 

button의 onClick 이벤트로도 처리할 수 있지만, onSubmit은 Enter를 눌러도 제출되는 반면 onClick은 말 그대로 click만 받아드립니다.

Enter를 통해서도 제출을 원한다면 onKeyPress 이벤트를 또 작성해줘야 하는 것이죠.

 

그럼 일정을 추가해보도록 하겠습니다.

잘 추가되는 것을 확인할 수 있습니다!