리다기 10장

슬기로운 FE 세상·2022년 1월 28일
2

prettier를 설정하여 코드를 작성할 때 깔끔하게 정리할 수 있습니다.. prettier 설정

{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all",
    "printWidth": 80
}

index.css를 수정하였습니다.

body {
  margin: 0;
  padding: 0;
  background: #e9ecef;
}

App 컴포넌트 초기화를 위해

import React from 'react'

const App = () => {
    return <div>Todo App을 만들자</div>;
}

export default App;

TodoTemplate: 화면을 가운데에 정렬시켜 주며, 앱 타이틀을 보여줍니다.
TodoInsert : 새로운 항목을 입력하고 추가할 수 있는 컴포넌트입니다. state를 통해 인풋의 상태를 관리합니다.
TodoListItem: 각 할 일 항목에 대한 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여 줍니다.
TodoList: todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 변환하여 보여줍니다.

src에 components라는 디렉터리를 생성하여 그 안에 저장하는대,, 이것이 기능, 구조상 필요해서가 아니라 관습입니다.


여기서 사용하고 싶은 아이콘을 고른 다음, import 구문을 사용하여 불러온 후 컴포넌트처럼 사용하면 됩니다.

App.js

import React, { useState,useRef,useCallback } from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList'
const App = () => {
  const [todos, setTodos] = useState([
  {
    id: 1,
    text: '리액트의 기초 알아보기',
    checked: true,
  },
  {
    id: 2,
    text: '컴포넌트 스타일링해보기',
    checked: true,
  },
  {
    id: 3,
    text: '일정 관리 앱 만들어보기',
    checked: false,
  },
  ])
  // 고윳값으로 사용될 id
  // ref를 사용하여 id 변수 담기
  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],
  )
  const onRemove = useCallback(
    id => {
      setTodos(todos.filter(todo => todo.id !== id));
    },
    [todos],
  )
  const onToggle = useCallback(
    id => {
      setTodos(
        todos.map(todo => 
          todo.id === id ? {...todo, checked: !todo.checked} : todo,
        )
      )
    },
    [todos],
  )
  return (
  <TodoTemplate>
    <TodoInsert onInsert={onInsert}/>
    <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
  </TodoTemplate>
  )
};

export default App;

App.js에 TodoTemplate, TodoInsert, TodoList를 렌더링합니다.
todos 배열 안에 들어 있는 객체에는 고유id, 내용, 완료 여부를 알려주는 값이 포함되어 있습니다. TodoList에 props로 전달됩니다. TodoList에서 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링합니다.
onInsert 함수는 todos 배열에 새 객체를 추가하는 함수입니다. id 값은 useRef를 사용하여 관리합니다. useState가 아닌 useRef를 사용하는 이유는 id 값은 렌더링 정보가 되는것이 아니라 항목을 만들 때 참조하는 값이기 때문에 useRef를 사용합니다. 또한 useCallback으로 함수를 감싸서 props를 전달할 때 재시작 하는것이 아닌, 재사용을 할 수 있게 하여 컴포넌트의 성능을 향상 시켰습니다.onInsert 함수는 TodoInsert 컴포넌트의 props로 설정합니다. onRemove는 filter 함수를 통해 같은 id를 가진 항목을 배열에서 지우는 함수입니다. onToggle 함수는 수정하는 기능인데 마찬가지로 App.js -> TodoList -> TodoListItem으로 전달됩니다. 하나의 원소만 바뀌면 되는데 map을 사용하는 이유는, todo.id === id ? --- : --- 의 모습인 삼항 연산를 사용하여, 같은 id 때는 새로운 객체를 생성하지만 , 다를 경우는 처음 그대로를 반환하기 때문에 map이 필요합니다.

TodoList.js

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

const TodoList = ({ todos,onRemove,onToggle }) => {
    return (
        <div className="TodoList">
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id} onRemove={onRemove} onToggle={onToggle} />
            ))}
        </div>
    )
}
export default TodoList;

props로 받아온 todos 배열을 배열 내장 함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링하여 줍니다.
여기서의 key 값은 각 항목마다 가지고 있는 고유값인 id를 넣어줍니다. 그리고 todo 데이터는 통째로 props로 전달해줍니다. TodoListItem에서 onRemove 함수를 사용하기 위해선 TodoList 컴포넌트를 거쳐야 하기 때문에, todos 배열을 TodoListItem에 그대로 전달해줍니다.

TodoListItem.js

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


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



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



export default TodoListItem;

className="remove"에 onClick 이벤트를 onRemove() 함수를 줌으로써, 자신이 가진 id를 삭제하도록 합니다. TodoList에도 onToggle 함수를 추가하여, onToggle 함수를 TodoListItem에서도 호출할 수 있도록 하였습니다.

TodoInsert.js

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

const TodoInsert = ({ onInsert }) => {
    const [value, setValue] = useState('');

    const onChange = useCallback(e => {
        setValue(e.target.value);
    },[]);

    const onSubmit = useCallback(
        e => {
            onInsert(value);
            setValue('');
            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;

TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState('')라는 상태를 정의하였고, onChange 함수를 통해 추가로 인풋에 넣어줄 수 있도록 하였습니다. 또한 useCallBack Hook을 사용하여 리렌더링 될 때 마다 함수를 새로 만드는 것이 아니라, 재사용을 할 수 있도록 하였습니다. onSubmit이라는 함수를 만들고, form의 onSubmit으로 설정합니다. 이 함수가 호출되면 props로 받아온 onInsert 함수에 value 값을 파라미터로 넣어 호출하고, 현재 value 값을 초기화합니다.
e.preventDefault()는 onSubmit 이벤트가 브라우저를 새로고침하기 때문에 이를 방지하는 용도로 사용합니다. onClick으로도 onSubmit 이벤트처럼 처리할 수 있습니다.

const onClick = useCallback( () => {
onInsert(value);
setValue('');
},
[onInsert, value],
);

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

이처럼 클릭 이벤트로 처리할 수 있는데 form, onSubmit으로 사용한 이유는 onSubmit 이벤트는 엔터키를 눌렀을 때도 발동하는 반면, onClick만 한다면 엔터를 처리하는 로직을 onKeyPress로 작성해야 하기 때문입니다.

profile
자 드가자~~

1개의 댓글