Step by Step

React Study(9) - 리액트를 다루는 기술 10장 본문

React

React Study(9) - 리액트를 다루는 기술 10장

짤진이 2023. 2. 8. 18:44
반응형

#10장 일정관리 웹 애플리케이션 만들기

구성 순서 : 프로젝트 준비하기 -> UI 구성하기 -> 기능 구현하기

10.1 프로젝트 생성 및 필요 라이브러리 설치

<index.css>

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

<App.js>
const App = () => {
  return <div>Todo App을 만들자!</div>
}
export default App;



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

10.2.1 TodoTemplate 만들기

<TodoTemplate.scss>

.TodoTemplate {
    width: 512px; //width가 주어진 상태에서 좌우 중앙 정렬
    margin-left: auto;
    margin-right: auto;
    margin-top: 6rem;
    border-radius: 4px;
    overflow: hidden;

    .app-title{
        background: #22b8cf;
        color:white;
        height:4rem;
        font-size: 1.5rem;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .content{
        background: white;
    }
}


10.2.2 TodoInsert 만들기

<TodoInsert.js>

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

const TodoInsert = () => {
    return (
        <form className="TodoInsert">
            <input placeholder="할 일을 입력하세요."/>
            <button type="submit"> 
                <MdAdd/>
            </button>
        </form>
    );
};
export default TodoInsert;

사용하고 싶은 기호의 이름을 MdAdd처럼 넣으면 사용 가능하다.


<TodoInsert.scss>

.TodoInsert{
    display:flex;
    background: #495057;
    input{
        //기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        padding: 0.5rem;
        font-size: 1.125rem;
        line-height: 1.5;
        color: white;
        &::placeholder{
            color:#dee2e6;
        }
        //버튼을 제외한 영역을 모두 차지하기
        flex: 1;
    }
    button{
        background: none;
        outline:none;
        border:none;
        background:#868e96;
        color: white;
        padding-left: 1rem;
        padding-right:1rem;
        font-size: 1.5rem;
        display: flex;
        align-items:center;
        cursor: pointer;
        transition: 0.1s background ease-in;
        &:hover{
            background:#adb5bd;
        }
    }
}



10.2.3 TodoListItem 과 TodoList 만들기
일정 관리 항목이 보일 TodoListItem과 TodoList를 만들 차례이다

<TodoListItem.js>

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

const TodoListItem = () => {
    return (
        <div className="TodoListItem">
            <div className="checkbox">
                <MdCheckBoxOutlineBlank/>
                <div className="text">할 일</div>
            </div>
            <div className="remove">
                <MdRemoveCircleOutline/>
            </div>
        </div>
    );
};
export default TodoListItem;


<TodoListItem.scss>

.TodoListItem{
    padding: 1rem;
    display: flex;
    align-items:center;
    &:nth-child(even){
        background: #f8f9fa;
    }
    .checkbox{
        cursor:pointer;
        flex: 1;
        display: flex;
        align-items: center; // 세로 중앙 정렬
        svg{
            //아이콘
            font-size: 1.5rem;
        }
        .text{
            margin-left: 0.5rem;
            flex : 1;//차지할 수 있는 모든 영역 차지 
        }
        //체크되었을 때 보여줄 스타일
        &.checked{
            svg{
                color:#22b8cf;
            }
            .text{
                color:#adb5bd;
                text-decoration: line-through;
            }
        }
    }
    .remove{
        display: flex;
        align-items: center;
        font-size: 1.5rem;
        color: #ff6b6b;
        cursor: pointer;
        &:hover{
            color: #ff8787;
        }
    }
    & + & {
        border-top: 1px solid #dee2e6;
    }
}



<TodoList.js>

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

const TodoList = () => {
    return (
        <div className>
            <TodoListItem/>
            <TodoListItem/>
            <TodoListItem/>
        </div>
    );
};
export default TodoList;


<TodoList.scss>

.TodoList{
    min-height: 320px;
    max-height: 513px;
    overflow-y: auto;
}

 


10.3 기능 구현하기

10.3.1 App에서 todos 사용하기
나중에 추가할 일정 항목에 대한 상태들은 모두 App 컴포넌트에서 관리한다.
App에서 useState를 사용하여 todos라는 상태를 정의하고 todos를 TodoList의 props로 전달한다.

<App.js>

import {useState} 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,
    },
  ]);
  return (
    <TodoTemplate>
      <TodoInsert/>
      <TodoList todos={todos}/>
    </TodoTemplate>
  )
}
export default App;


<TodoListItem.js>

import {
    MdCheckBoxOutlineBlank,
    MdCheckBox,
    MdRemoveCircleOutline,
} from 'react-icons/md';
import cn from 'classnames';
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;


<TodoList.js>

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

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



10.3.2 항목 추가 기능 구현하기
TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에는 todos 배열에 새로운 객체를 추가하는 함수를 만들어준다.

10.3.2.1 TodoInsert value 상태 관리하기
TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의한다.
캄포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용해본다.

<TodoInsert.js>

import {useState, useCallback} from 'react';
import {MdAdd} from 'react-icons/md';
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;


10.3.2.3 todos 배열에 새 객체 추가하기
App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만든다.
새로운 객체를 만들 때마다 id값에 1씩 더해주어야 한다. id값은 useRef를 사용한다.

<App.js>

import {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를 사용하여 변수 담기
  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;


<TodoInsert.js>

import {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(''); // 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;


onSubmit이라는 함수를 만들고 이를 form의 onSubmit으로 지정했다.
이 함수가 호출되면 props로 받아온 onInsert함수에 현재 value 값을 파라미터로 넣어서 호출하고 현재 value값을 초기화한다.
onSubmit 대신에 onClick 이벤트로도 처리 가능하다.

const Onclick = useCallback(
    () => {
        onInsert(value);
        setValue(''); //value 값 초기화
    },
    [onInsert, value],
);

 


10.3.3 지우기 기능 구현하기
배열의 불변성을 지키면서 배열 원소를 제거해야 할 경우, 배열 내장 함수인 filter를 사용하면 매우 편하다.
[filter 사용 예제]
const array = [1,2,3,4,5,6,7,8,9];
const biggerThanFive = array.filter(number => number>5);
//결과 : [6,7,8,9]

<App.js>

const onRemove = useCallback(
    id => {
      setTodos(todos.filter(todo => todo.id !==id));
    },
    [todos],
  );
  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert}/>
      <TodoList todos={todos} onRemove = {onRemove}/>
    </TodoTemplate>
  )
}



<TodoList.js>

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

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



<TodoListItem.js>

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

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


10.3.4 수정 기능
onToggle이라는 함수를 App에 만들고 해당 함수를 TodoList 컴포넌트에 props로 넣어준다.

<App.js>

const onToggle = useCallback(
    id => {
      setTodos(
        todos.map(todo =>
          todo.id === id ? {...todo, checked: !todo.checked} : todo,
        ),
      );
    },
    [todos],
  )


map을 사용하여 특정 id를 가지고 있는 객체의 checked 값을 반전시켰다.
불변성을 유지하면서 특정 배열의 원소를 업데이트할때는 map을 사용한다.
map 함수는 배열을 전체적으로 새로운 형태로 변환하여 새로운 배열을 생성해야할 때 사용한다고 배웠다.
onToggle 함수를 보면 todo.id === id ? : 이라는 삼항 연산자가 사용되었다.
todo.id와 현재 파라미터로 사용된 id값이 같을 때 새로운 객체를 생성하지만 다를때는 변화를 주지않고 처음 상태 그대로 반환한다.

10.4 정리
....

반응형