본문 바로가기
Frontend/ReactJS(완)

React - 10장 : To-do 앱 실습(컴포넌트 렌더링최적화, Hooks 활용)

by VictorMeredith 2023. 2. 17.

<리액트를 다루는 기술>

React - 1장 : React 이해

React - 2장 : JSX

React - 3장 : 컴포넌트

React - 4장 : 이벤트 핸들링

React - 5장 : ref. DOM에 이름 달기 

React - 6장 : 컴포넌트 반복
React - 7장 : 컴포넌트의 LifeCycle
React - 8장 : React Hooks 총정리

React - 9장 : 컴포넌트의 스타일링

React - 10장 : 빠르게 TODO앱 실습(현재)

 

누구나  만들어보는 투두앱을 빠르게 만들어서 컴포넌트최적화와 Hooks를 적용해보자.

10-1. 프로젝트 생성

- npx create-react-app todo-app

 

10-2. UI 구성

- TodoTemplate : 화면을 가운데에 정렬시켜 주며, 앱 타이틀을 보여준다. children으로 내부 JSX를 props로 받아 와서 렌더링해준다.

- TodoInsert : 새로운 항목을 입력하고 추가할 수 있는 컴포넌트. state를 통해 인풋의 상태를 관리한다.

- TodoListItem : 각 할 일 항목에 대한 정보를 보여주는 컴포넌트. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여준다.

- TodoList : todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 변환하여 보여준다.

디렉토리 구조

10-2.1 TodoTemplate

TodoTemplate.jsx

todoTemplate.jsx

- 다음으로 이 컴포넌트를 App.js에서 불러와 렌더링한다.

App.js

App.js

- sass가 없을 경우 npm i sass 명령어로 설치해준다.

 

TodoTemplate.scss

TodoTemplate.scss

- scss인데 네스팅 안써요? : 네스팅하면 스타일링 수정할 때 브라우저에서 ctrl+c -> v 해서 찾는게 느립니다.

- 왜 엔터쳐서 안함? : 개취입니다. 한줄로 쓰는게 구조파악에 1000억배는 유리합니다

참고로 누가 엔터쳐서 작성한 css 한줄로 만드는 one-line-formatter라는 라이브러리 있습니다 컨트롤j만 누르면 알아서 저렇게 해줌

다시 엔터치는 방식으로 돌리려면 뷰티파이같은거 쓰세요

- css파일을 각각 만드나요 ? : 파일을 엄청나게 많이 나누지는 않습니다. 구분단위는 회사 퍼블리셔 마음임. 

 

현재까지 결과물

10-2.2 TodoTemplate

TodoInsert.jsx

TodoInsert.jsx

- npm i react/icons 로 아이콘을 설치해주자.

 

TodoInsert.scss

TodoInsert.scss

10-2.3 TodoListItem과 TodoList 만들기

TodoListItem.jsx

TodoListItem.jsx

TodoList.jsx

TodoList.jsx

- 현재는 props 전달 없이 그대로 여러번 보여주나, 기능을 추가할 예정이다.

- 이를 App.js 에 렌더링해준다.

 

App.js

App.js

TodoList.scss

TodoList.scss

TodoListItem.scss

TodoListItem.scss

- 현재까지 스타일링 된 상태

현재까지 상태

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';
import './App.css';

function App() {

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

  return (
    <div className="App">
      <TodoTemplate>
        <TodoInsert/>
        <TodoList todos={todos} />
      </TodoTemplate>
    </div>
  );
}

export default App;

- todos 배열 안에 들어 있는 개체에는 각 항목의 고유 id, 내용, 완료 여부를 알려주는 값이 포함되어 있습니다.

- 이 배열 TodoList에 props로 전달되는데요, TodoList에서 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정한다.

 

TodoList.jsx

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

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

export default TodoList;

 

TodoListItem.jsx

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;

현재까지 props를 전달해서 UI를 배치한 결과

10-3.2 항목 추가 기능 구현하기

- TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에는 todos 배열에 새로운 객체를 추가하는 함수를 만들어 주어야 합니다.

 

10-3.2.1 TodoInsert value 상태 관리하기

TodoInsert.jsx

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

const TodoInsert = ()=>{

    const [value, setValue] = useState('');

    const onChange = useCallback(e => { 
        //컴포넌트가 리렌더링 될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용한다.
        setValue(e.target.value);
    } ,[])

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

export default TodoInsert;

- 이제는 input field에 글을 입력할때, 지울때마다 onChange함수가 실행된다 !

10-3.2.3 todos 배열에 새 객체 추가하기

- 이번에는 App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만든다.

- useState가 아닌 useRef로 사용하여 컴포넌트에서 사용 할 변수를 만드는 이유는 ? => id 값은 레더링되는 정보가 아니기 때문이다.

- 또한 onInsert 함수는 컴포넌트의 성능을 아낄 수 이또록 useCallback으로 감싸준다.

- props로 전달해야 할 함수를 만들 때는 useCallbaxk을 사용하여 함수를 감싸는 것을 습관화하면 좋다.

- onInsert 함수를 만든 뒤에는 해당 함수를 TodoInsert 컴포넌트의 props로 설정해 준다.

 

App.js

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

function 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 (
    <div className="App">
      <TodoTemplate>
        <TodoInsert onInsert={onInsert}/>
        <TodoList todos={todos} />
      </TodoTemplate>
    </div>
  );
}

export default App;

10-3.2.4 TodoInsert에서 onSubmit 이벤트 설정하기

- 이제는 버튼을 클릭하면 발생할 이벤트를 설정해본다.

- App에서 TodoInsert에 넣어준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출한다.

 

TodoInsert.jsx

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 => { 
        //컴포넌트가 리렌더링 될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용한다.
        setValue(e.target.value);
    } ,[])

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

    return (
        <div className="TodoInsert">
            <input type="text" placeholder='할 일을 입력하세용' value={value} onChange={onChange}/>
            <button onClick={onClick}>
                <MdAdd/>
            </button>
        </div>
    )
}

export default TodoInsert;

 

- onClick 함수가 호출되면 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value 값을 초기화한다.

- 이제 일정추가기능이 구현되었다!

 

10-3.3 지우기 기능 구현

10-3.3.1 배열함수 filter

array.filter((e)=>{조건식})

10-3.3.2 todos 배열에서 id로 항목 지우기

- filter함수를 이용하여 onRemove 함수를 만들어보자.

 

App.js

// ...

function App() {

// ...

  const onRemove = useCallback(
    id => {
      setTodos(todos.filter(todo => todo.id !==id));
    },
    [todos]
  )

  return (
    <div className="App">
      <TodoTemplate>
        <TodoInsert onInsert={onInsert}/>
        <TodoList todos={todos} onRemove={onRemove} />
      </TodoTemplate>
    </div>
  );
}

export default App;

10-3.3.3 TodoListItem에서 삭제 함수 호출하기

- TodoListItem에서 onRemove함수를 사용하려면 props로 TodoList에 우선 내려야한다.

 

TodoList.jsx

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

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

export default TodoList;

TodoListItem.jsx

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.js에 만들고, props로 TodoList -> TodoListItem까지 전달해준다.

 

App.js

//...
function App() {

//...

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

  return (
    <div className="App">
      <TodoTemplate>
        <TodoInsert onInsert={onInsert}/>
        <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
      </TodoTemplate>
    </div>
  );
}

export default App;

-TodoList를 통해 TodoListItem에 props로 onToggle함수를 전달하고 check박스 부분에 onClick으로 바인딩해준다.

 

TodoListItem.jsx

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;

- 기능 구현 모두 완료 !

완성

- 다음은 방금 만든 프로젝트의 렌더링 성능 최적화에 대해 포스팅!

댓글