ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 개발자 지망생 스터디 - 27일차
    스터디/KAKAOCLOUDSCHOOL 2022. 12. 8. 10:11

    ToDo Application

    1.프로젝트 생성하고 필요한 라이브러리

    1) 프로젝트 생성

    $yarn create react-app react-todo

    2) 필요한 라이브러리

    $yarn add sass-loader sass classnames react-icons open-color
    > sass-loader : scss 파일을 사용하기 위해서 설치
    > sass : scss 파일을 사용하기 위해서 설치
    > classnames : css를 작성할 classname을 편리하게 작성하기 위한 라이브러리
    > react-icons : 아이콘을 사용하기 위한 라이브러리
    > open-color : 색상을 직접 값으로 설정하는 것이 아니고 색상 이름과 정수 1개의 농도를 가지고 설정할 수 있도록 해주는 라이브러리,

    ❖ 참고
    https://react-icons.github.io/react-icons
    https://yeun.github.io/open-color/
     

    Open Color

    Color scheme for UI design

    yeun.github.io

     

    React Icons

    React Icons Include popular icons in your React projects easily with react-icons, which utilizes ES6 imports that allows you to include only the icons that your project is using. Installation (for standard modern project) npm install react-icons --save Usa

    react-icons.github.io

    3) index.css 수정

    # index.css 에 "padding : 0 과 backgroud : 색상" 코드 추가
    🗂react_todo → 📁src → 📄 index.css

    body {
      margin: 0;
      padding : 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
        'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
        sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      background: black;
    }
    
    code {
      font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
        monospace;
    }​

     


    # Web Application 에서 body 나 모든 box 태그에 margin 과 padding을 0 으로 설정하는 경우
    > IE 구버전과 호환성 때문
    > IE 구버전은 width와 height 안에 padding 과 margin 그리고 border의 크기가 포함되고 나머지 브라우저는 content 의 크기만 포함 됨

    4) App.js 수정

    #프로젝트를 생성하면서 자동 생성된 코드에서 필요 없는 코드를 지우고 기본적인 코드로 수정 
    function App() {
      return (
        <div>
          To Do Application
        </div>
      );
    }
    
    export default App;​

    2. UI

    1)구성

    # ToDoTemplate : Main
    # ToDoInsert : 데이터 삽입을 위해 하나의 input 과 버튼을 가진 화면
    # ToDoListItem : 하나의 항목을 출력하기 위한 컴포넌트
    # ToDoList : ToDoListItem의 목록의 출력하기 위한 컴포넌트

    ☑︎ ToDoListItem 을 ToDoList에 배치하고
        ToDoList 와 ToDoInsert를 ToDoTemplate에 배치해서
        ToDoTemplate을 App에 배치
    ☑︎ ToDo 의 내용은 구분하기 위한 값, 내용, 실행 여부로 구성
    ☑︎ 모든 컴포넌트와 scss 파일은 components 디렉토리에 배치

    > 이 부분은 컴포넌트 와 scss 파일과 index.js 로 묶어서 별도의 디렉토리로 구성해도 됨.
    > 이 경우 디렉토리의 이름은 컴포넌트의 이름과 같아야 함
    > 이렇게 하는 것이 재사용성을 증가 시킴
    > node를 기반으로 하는 프로젝트에서 index.js 의 역할른 디렉토리 안의 모든 것들을 외부에서 사용할 수 있도록 export 하는 것
    > require 나 import 할 때 디렉토리 이름을 사용하면 디렉토리 안에 있는 index.js 파일에서 export한 내용을 가져옴

    # src 디렉토리에 컴포넌트 관련 파일을 저장하기 위한 디렉토리 생성
    🗂react_todo → 📁 src → (CREATE) 📁 component

    2) Main 화면 - ToDoTemplate

    # ToDoTemplate.jsx 파일 생성
    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoTemplate.jsx
    import React from "react";
    import './ToDoTemplate.scss';
    //함수형 컴포넌트에서 매개변수(children)는 상위 컴포넌트에서 하위 컴포넌트를 만들 때 태그에서 넘겨준 속성들임
    const ToDoTemplate = ({children}) => {
        return(
            <div className='ToDoTemplate'>
                <div className='app-title'>일정 관리</div>
                <div className='content'>
                    {children}
                </div>
            </div>
        )
    }
    export default ToDoTemplate;

    # App.js 수정
    import ToDoTemplate from "./components/ToDoTemplate";
    
    function App() {
      return (
        <ToDoTemplate>
          To Do Application
        </ToDoTemplate>
      );
    }
    
    export default App;​

    # ToDoTemplate.scss 파일을 생성하고 작성
    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoTemplate.scss
    .ToDoTemplate{
        width: 512px;
        margin-left: auto;
        margin-right: auto;
        margin-top: 6rem;
        border-radius: 4px;
        overflow: hidden;
    
        .app-title{
            background: #343a40;
            color: white;
            height: 4rem;
            font-size: 1.5rem;
            align-items: center;
            justify-content: center;
        }
        .content{
            background: white;
        }
    }​

    3) 데이터 삽입 화면 - input 하나와 버튼( 아이콘) 하나를 배치

    # ToDoInsert.jsx 파일 생성 후 작성
    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoInsert.jsx
    import React from "react";
    
    //react-icons 의 MaterialDesign 의 MdAdd 라는 아이콘 사용
    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;

    # ToDoInsert.scss 파일을 생성하고 작성

    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoInsert.scss
    .ToDoInsert{
        display: flex;
        background: #dee2e6;
        input{
            background: none;
            outline: none;
            border: none;
            padding: 0.5rem;
            font-size: 1.125rem;
            line-height: 1.5;
            color: white;
            &::placeholder{
                color : grey;
            }
            flex : 1;
            /* 상위 태그에 flex 가 설정되어 있으면 전체 합에 대한 비율로 배치가 가능함 - bootstrap 많이 이용 */
        }
    
        button{
            background: none;
            outline: none;
            border: none;
            background: gray;
            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: #adb5ad;
            }
        }
    }​

    4) 데이터 목록 화면 - ToDoListItem(하나의 항목 출력) 과 ToDoList(ToDoListItem을 List로 출력)

    # 하나의 항목을 출력할 ToDoListItem.jsx 파일을 만들고 작성
    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoListItem.jsx
    import React from "react";
    
    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의 디자인을 위한 ToDoListItem.scss 파일을 만들고 작성
    🗂react_todo → 📁 src → 📁 component → (CREATE) 📄 ToDoListItem.scss
    .ToDoListItem{
        padding: 1rem;
        display: flex;
        align-items: center;
        &:nth-child(even){
            background: #ced4da;
        }
        .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:aqua;
                }
                .text{
                    color: aquamarine;
                    text-decoration: line-through;
                }
            }
        }
        .remove{
            display: flex;
            align-items: center;
            font-size: 1.5rem;
            color: red;
            cursor: pointer;
            &:hover{
                color: #ff8787;
            }
        }
        & + &{
            border-top: 1px solid #dee2e6;
        }
    }

    5) ToDoList

    #ToDoList.jsx
    🗂react_todo → 📁 src → 📁 component → 📄 ToDoList.jsx
    import React from "react";
    import ToDoListItem from "./ToDoListItem";
    import "./ToDoList.scss"
    
    const ToDoList = () => {
        return(
            <div className="ToDoList">
                <ToDoListItem />
                <ToDoListItem />
                <ToDoListItem />
                <ToDoListItem />
                <ToDoListItem />
            </div>
        )
    }
    export default ToDoList;

    #ToDoList.scss
    🗂react_todo → 📁 src → 📁 component → 📄 ToDoList.scss
    .ToDoList{
        min-height: 320px;
        max-height: 513px;
        overflow-y: auto; //데이터가 쌓이면 스크롤바가 생성
    }

    3. 기능 구현

    1) 데이터 배열 출력

    App.js 파일을 수정해서 샘플 데이터 배열을 state(데이터가 수정되면 컴포넌트를 리랜더링)로 생성하고 ToDoList에게 전달(상위컴포넌트에서 하위 컴포넌트에게 데이터를 전달할 때는 props를 사용) 

    #App.js 수정
    🗂react_todo → 📁 src → 📄 App.js
    import { useState } from "react";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    import ToDoTemplate from "./components/ToDoTemplate";
    
    function App() {
      const [todos, setToDos] = useState([
        {
          id:1,
          text:'HTML, CSS, JacaScript',
          checked : true
        },
        {
          id:2,
          text:'Node.js',
          checked : true
        },
        {
          id:3,
          text:'React',
          checked : true
        },
        {
          id:4,
          text:'JAVA',
          checked : true
        },
        {
          id:5,
          text:'Python',
          checked : true
        }
      ])
      return (
        <ToDoTemplate>
          <ToDoInsert/>
          <ToDoList todos={todos}/>
        </ToDoTemplate>
      );
    }
    
    export default App;​


    # ToDoList.jsx 파일에서 데이터를 넘겨받아서 ToDoListItem 에서 출력하도록 설정
    🗂react_todo → 📁 src → 📁 component → 📄 ToDoList.jsx
    import React from "react";
    import ToDoListItem from "./ToDoListItem";
    import "./ToDoList.scss"
    
    const ToDoList = ({todes}) => {
        return(
            <div className="ToDoList">
                {
                    ToDoList.map(todo => (
                        <ToDoListItem todo={todo} key={todo.id}/>
                        )
                    )
                }
            </div>
        )
    }
    export default ToDoList;​

    # ToDoListItem.jsx 파일을 수정해서 넘어온 데이터 출력

    🗂react_todo → 📁 src → 📁 component → 📄 ToDoListItem.jsx

    import React from "react";
    
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md'
    
    import cn from 'classnames';
    
    import "./ToDoListItem.scss";
    
    const ToDoListItem = ({todo}) => {
        //넘어온 데이터 중에서 text와 checked 만 분해
        const {text, checked} = todo;
        return(
            <div className="ToDoListItem">
                <div className={cn("checkbox", {checked})}>
                    {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                    <div className="text">할 일</div>
                </div>
                <div className="remove">
                    <MdRemoveCircleOutline/>
                </div>
            </div>
        )
    }
    export default ToDoListItem;​

    2) 데이터 추가 기능 - 데이터를 처리하는 함수는 App.js(state가 App.js에 존재) 에 만들어서 넘겨주는 구조

    #App.js 파일을 수정해서 데이터 삽입 관련된 함수를 만들고 삽입 컴포넌트에게 넘겨주기
    🗂react_todo → 📁 src → 📄 App.js
    import ToDoTemplate from "./components/ToDoTemplate";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import {useState, useRef, useCallback} from 'react';
    
    function App() {
      const [todos, setToDos] = useState([
        {
          id:1,
          text:'HTML, CSS, JavaScript',
          checked:true
        },
        {
          id:2,
          text:'Node.js',
          checked:true
        },
        {
          id:3,
          text:'MariaDB, MongoDB',
          checked:true
        },
        {
          id:4,
          text:'React',
          checked:false
        },
      ]);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(5);
    
      //삽입을 처리하기 위한 함수
      //todos 에 변화가 생기면 함수를 만들지만 그렇지 않으면
      //기존 함수를 이용
      const onInsert = useCallback((text) => {
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        setToDos(todos.concat(todo));
        nextId.current += 1;
      }, [todos]);
    
      return (
        <ToDoTemplate>
          <ToDoInsert onInsert={onInsert}/>
          <ToDoList todos={todos}/>
        </ToDoTemplate>
      );
    }
    
    export default App;​

     

    # ToDoInsert.jsx 파일에서 데이터 삽입 이벤트 와 연결
    🗂react_todo → 📁 src → 📁 component → 📄 ToDoInsert.jsx
    import React from "react";
    
    //react-icons 의 MaterialDesign 의 MdAdd 라는 아이콘 사용
    import { MdAdd } from 'react-icons/md';
    import "./ToDoInsert.scss";
    import { useState, useCallback } from "react";
    
    const ToDoInsert = ({onInsert}) => {
        //입력된 데이터를 저장하기 위한 state
        const [value, setValue] = useState('');
        //입력 내용이 변경될 때 호출될 함수
        const onChange = useCallback((e)=>{
            setValue(e.target.value);
        }, []);
        //form에서 submit 이벤트가 발생하면 호출될 함수
        //form안에서 submit 버튼을 눌러도 submit 이벤트가 발생하지만
        //form안에서 Enter를 눌러도 submit 이벤트는 발생함
        const onSubmit = useCallback((e)=>{
            const result = window.confirm(`추가할 내용 :${value}`);
            if(result === false){
                e.prventDefault();
                return;
            }
            //데이터 삽입
            onInsert(value);
            //input 초기화
            setValue('');
            //제공되는 기본 이벤트 처리 코드를 수행하지 않음
            //form의 submit 이나 a 태그는 화면전체를 갱신하기 때문에 이전 내용을 모두 잃어버림
            e.prventDefault();
        },[onInsert, value]);
        return(
            <form className="ToDoInsert" onSubmit={onSubmit}>
                <input placeholder="할 일을 입력하세요"
                value={value} onChange={onChange}/>
                <button type="submit"><MdAdd/></button>
            </form>
        )
    }
    export default ToDoInsert;

    3) 데이터 삭제 기능 구현

    #App.js 파일에 삭제 함수를 추가하고 ToDoList 에게 전달
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    import ToDoTemplate from "./components/ToDoTemplate";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import { useState, useRef, useCallback } from "react";
    
    function App() {
      const [todos, setToDos] = useState([
        {
          id:1,
          text:'HTML, CSS, JacaScript',
          checked : true
        },
        {
          id:2,
          text:'Node.js',
          checked : true
        },
        {
          id:3,
          text:'React',
          checked : true
        },
        {
          id:4,
          text:'JAVA',
          checked : true
        },
        {
          id:5,
          text:'Python',
          checked : true
        }
      ]);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(5);
    
      //삽입을 처리하기 위한 함수
      //todos에 변화가 생기면 함수를 만들지만 그렇지 않으면 기존 함수를 이용
      const onInsert = useCallback((text)=>{
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        setToDos(todos.concat(todo));
        nextId.current += 1;
      },[todos]);
      
      const onRemove = useCallback((id)=>{
        setToDos(todos.filter(todo => todo.id !== id));
      }, [todos])
    
      return (
        <ToDoTemplate>
          <ToDoInsert onInsert={onInsert}/>
          <ToDoList todos={todos} onRemove={onRemove}/>
        </ToDoTemplate>
      );
    }
    
    export default App;​

    # ToDoList 에서 데이터 삭제 함수를 ToDoListItem에게 넘겨주도록 수정
    import React from "react";
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md'
    import cn from 'classnames';
    import "./ToDoListItem.scss";
    import { useCallback } from "react";
    
    const ToDoListItem = ({todo, onRemove}) => {
        //넘어온 데이터 중에서 text와 checked 만 분해
        const {id, text, checked} = todo;
        //data삭제 함수
        const onDelete = useCallback((e)=> {
            const result = window.confirm(text + "를 정말로 삭제");
            if(result){
                onRemove(id);
            }
        },[onRemove,id,text]);
    
        return(
            <div className="ToDoListItem">
                <div className={cn("checkbox", {checked})}>
                    {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                    <div className="text">{text}</div>
                </div>
                <div className="remove" onClick={onDelete}>
                    <MdRemoveCircleOutline/>
                </div>
            </div>
        )
    }
    export default ToDoListItem;​


    # ToDoListItem.jsx 파일에 삭제 구현

    import React from 'react';
    
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md';
    
    import cn from 'classnames';
    
    import "./ToDoListItem.scss";
    
    import { useCallback } from 'react';
    
    const ToDoListItem = ({todo, onRemove}) => {
        //넘어온 데이터 중에서 text 와 checked 만 분해
        const {id, text, checked} = todo;
        //데이터 삭제 함수
        const onDelete = useCallback((e) => {
            const result = window.confirm(text + "를 정말로 삭제");
            if(result){
                onRemove(id);
            }
        }, [onRemove, id, text]);
    
        return(
            <div className="ToDoListItem">
                <div className={cn("checkbox", {checked})}>
                    {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                    <div className='text'>{text}</div>
                </div>
                <div className="remove" onClick={onDelete}>
                    <MdRemoveCircleOutline />
                </div>
            </div>
        )
    }
    
    export default ToDoListItem;​

    4) 데이터 수정

    > 데이터 목록화면에서 데이터의 체크박스를 클릭하면 checked 의 상태가 변경되도록 설정

    #App.js 파일에 id를 넘겨받아서 id에 해당하는 데이터의 checked 값을 Toggle(반전)하는 함수를 만들고 ToDoList 컴포넌트에 넘겨주도록 작성
    import ToDoTemplate from "./components/ToDoTemplate";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import {useState, useRef, useCallback} from 'react';
    
    function App() {
      const [todos, setToDos] = useState([
        {
          id:1,
          text:'HTML, CSS, JavaScript',
          checked:true
        },
        {
          id:2,
          text:'Node.js',
          checked:true
        },
        {
          id:3,
          text:'MariaDB, MongoDB',
          checked:true
        },
        {
          id:4,
          text:'React',
          checked:false
        },
      ]);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(5);
    
      //삽입을 처리하기 위한 함수
      //todos 에 변화가 생기면 함수를 만들지만 그렇지 않으면
      //기존 함수를 이용
      const onInsert = useCallback((text) => {
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        setToDos(todos.concat(todo));
        nextId.current += 1;
      }, [todos]);
    
      //데이터 삭제를 위한 함수
      const onRemove = useCallback((id) => {
        setToDos(todos.filter(todo => todo.id !== id));
      }, [todos])
    
      //데이터 수정을 위한 함수
      const onToggle = useCallback((id) => {
        //todos를 복제해서 하나씩 순회하면서
        //todo의 id 값 과 매개변수로 받은 id가 일치하면 
        //checked를 반전하고 그렇지 않으면 그대로
        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;​


    # ToDoList 컴포넌트에서 넘겨받은 함수를 ToDoListItem 컴포넌트에게 넘겨주도록 수정
    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;

    # ToDoListItem 컴포넌트에 수정 함수를 구현
    import React from 'react';
    
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md';
    
    import cn from 'classnames';
    
    import "./ToDoListItem.scss";
    
    import { useCallback } from 'react';
    
    const ToDoListItem = ({todo, onRemove, onToggle}) => {
        //넘어온 데이터 중에서 text 와 checked 만 분해
        const {id, text, checked} = todo;
        //데이터 삭제 함수
        const onDelete = useCallback((e) => {
            const result = window.confirm(text + "를 정말로 삭제");
            if(result){
                onRemove(id);
            }
        }, [onRemove, id, text]);
    
        return(
            <div className="ToDoListItem">
                <div className={cn("checkbox", {checked})} 
                    onClick={(e)=>onToggle(id)}>
                    {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                    <div className='text'>{text}</div>
                </div>
                <div className="remove" onClick={onDelete}>
                    <MdRemoveCircleOutline />
                </div>
            </div>
        )
    }
    
    export default ToDoListItem;


    4. 최적화

    1) 많은 양의 데이터 랜더링

    # 많은 양의 데이터를 생성해서 출력하도록 App.js 수정
    import ToDoTemplate from "./components/ToDoTemplate";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import {useState, useRef, useCallback} from 'react';
    
    //대량의 데이터를 생성해서 리턴하는 함수
    const createBulkTodos = () => {
      const array = [];
      for(let i = 1; i<= 2000; i++){
        array.push({
          id:i,
          text:`할 일 ${i}`,
          checked:false
        })
      }
      return array;
    }
    
    
    function App() {
      //useState 에 데이터를 생성하는 함수를 대입할 때 
      //함수 호출 구문을 대입하면 데이터가 만들어 질 때 마다
      //리랜더링을 합니다.
      //함수 이름을 대입해야 함수를 전부 수행하고 1번만 리랜더링을 수행합니다.
      const [todos, setToDos] = useState(createBulkTodos);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(2001);
    
      //삽입을 처리하기 위한 함수
      //todos 에 변화가 생기면 함수를 만들지만 그렇지 않으면
      //기존 함수를 이용
      const onInsert = useCallback((text) => {
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        setToDos(todos.concat(todo));
        nextId.current += 1;
      }, [todos]);
    
      //데이터 삭제를 위한 함수
      const onRemove = useCallback((id) => {
        setToDos(todos.filter(todo => todo.id !== id));
      }, [todos])
    
      //데이터 수정을 위한 함수
      const onToggle = useCallback((id) => {
        //todos를 복제해서 하나씩 순회하면서
        //todo의 id 값 과 매개변수로 받은 id가 일치하면 
        //checked를 반전하고 그렇지 않으면 그대로
        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;

     

    2) 컴포넌트가 리랜더링 되는 경우

    > 전달받은 props 가 변경되는 경우
    > 자신의 state가 변경되는 경우
    > 상위 컴포넌트가 리랜더링 되는 경우
    > forceUpdate 함수를 호출하는 경우

    3) 하나의 데이터가 수정되면 전체가 리랜더링 문제를 해결

    > 현재 컴포넌트가 2000개 인데 하나의 데이터에 수정이 발생하면 todos 에 변경이 일어나고 todos는 App 컴포넌트의 state 이므로 App이 리랜더링을 할 텐데 이렇게 되면 화면 전체가 리랜더링되는 것과 같음
    > 자신의 props 가 변경될 때만 리랜더링하도록 할 수 있는데 Class Component 에서는 shouldComponentUpdate 라는 수명주기 메서드를 이용하고 Function Component 에서는 React.memo를 이용하면 됨.
    컴포넌트를 React.memo 함수로 감싸주기만 하면 됨

    # ToDoListItem 컴포넌트가 자신의 props 가 변경된 경우에만 리랜더링 하도록 수정
    import React from 'react';
    
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md';
    
    import cn from 'classnames';
    
    import "./ToDoListItem.scss";
    
    import { useCallback } from 'react';
    
    const ToDoListItem = ({todo, onRemove, onToggle}) => {
        //넘어온 데이터 중에서 text 와 checked 만 분해
        const {id, text, checked} = todo;
        //데이터 삭제 함수
        const onDelete = useCallback((e) => {
            const result = window.confirm(text + "를 정말로 삭제");
            if(result){
                onRemove(id);
            }
        }, [onRemove, id, text]);
    
        return(
            <div className="ToDoListItem">
                <div className={cn("checkbox", {checked})} 
                    onClick={(e)=>onToggle(id)}>
                    {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                    <div className='text'>{text}</div>
                </div>
                <div className="remove" onClick={onDelete}>
                    <MdRemoveCircleOutline />
                </div>
            </div>
        )
    }
    
    export default React.memo(ToDoListItem);

     

    4) 함수가 업데이트 되지 않도록 하기

    > useCallback을 이용해서 함수를 선언했는데 useCallback을 이용하면 두번째 매개변수인 deps 배열의 데이터에 변경이 생기면 함수를 새로 생성 실제로 todos 배열에 변경이 생긴다고 해서 함수를 새로 만들 필요는 없음
    대부분의 경우 이벤트 처리 함수는 다시 만들어질 필요가 없음

    💡 해결책
    useState 의 setter 에 함수형 업데이트를 사용하도록 하면 됨.
    useReducer를 이용해서 함수를 컴포넌트 외부에 생성

    5)useState에 함수형 업데이트 사용

    # App.js 파일에서 삽입, 수정, 삭제를 위한 메서드를 수정
    import ToDoTemplate from "./components/ToDoTemplate";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import {useState, useRef, useCallback} from 'react';
    
    //대량의 데이터를 생성해서 리턴하는 함수
    const createBulkTodos = () => {
      const array = [];
      for(let i = 1; i<= 2000; i++){
        array.push({
          id:i,
          text:`할 일 ${i}`,
          checked:false
        })
      }
      return array;
    }
    
    
    function App() {
      //useState 에 데이터를 생성하는 함수를 대입할 때 
      //함수 호출 구문을 대입하면 데이터가 만들어 질 때 마다
      //리랜더링을 합니다.
      //함수 이름을 대입해야 함수를 전부 수행하고 1번만 리랜더링을 수행합니다.
      const [todos, setToDos] = useState(createBulkTodos);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(2001);
    
      //삽입을 처리하기 위한 함수
      //todos 에 변화가 생기면 함수를 만들지만 그렇지 않으면
      //기존 함수를 이용
      const onInsert = useCallback((text) => {
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        //함수형 업데이트
        setToDos(todos => todos.concat(todo));
        nextId.current += 1;
      }, []);
    
      //데이터 삭제를 위한 함수
      const onRemove = useCallback((id) => {
        setToDos(todos=>todos.filter(todo => todo.id !== id));
      }, [])
    
      //데이터 수정을 위한 함수
      const onToggle = useCallback((id) => {
        //todos를 복제해서 하나씩 순회하면서
        //todo의 id 값 과 매개변수로 받은 id가 일치하면 
        //checked를 반전하고 그렇지 않으면 그대로
        setToDos(todos => todos.map(todo => 
          todo.id === id ?{...todo, checked:!todo.checked}:todo))
      }, []);
    
      return (
        <ToDoTemplate>
          <ToDoInsert onInsert={onInsert}/>
          <ToDoList todos={todos} onRemove={onRemove} 
            onToggle={onToggle}/>
        </ToDoTemplate>
      );
    }
    
    export default App;

     

    6) useReducer를 이용하는 방법

    > state를 수정하는 함수를 컴포넌트 외부 내보내는 작업
    함수는 변경할 state 와 action 을 매개변수로 받아서 action의 type을 가지고 분기를 만들어서 state에 작업을 수행해주면 됨

    컴포넌트 내부에서는 useState를 사용하지 않고 useReducer(함수이름, 초기값, 초기화하는 함수)를 이용해서 state 와 state를
    수정하는 함수를 생성

    # App.js를 수정
    import ToDoTemplate from "./components/ToDoTemplate";
    import ToDoInsert from "./components/ToDoInsert";
    import ToDoList from "./components/ToDoList";
    //useRef는 변수를 생성하거나 변수를 만들어서 DOM에 할당하기 위해서
    //useCallback은 함수를 효율적으로 생성하기 위해서
    import {useRef, useCallback, useReducer} from 'react';
    
    //대량의 데이터를 생성해서 리턴하는 함수
    const createBulkTodos = () => {
      const array = [];
      for(let i = 1; i<= 2000; i++){
        array.push({
          id:i,
          text:`할 일 ${i}`,
          checked:false
        })
      }
      return array;
    }
    
    //state를 조작할 reducer 함수 생성
    const todoReducer = (todos , action) => {
      //분기
      switch(action.type){
        case 'INSERT':
          return todos.concat(action.todo);
        case 'REMOVE':
          return todos.filter(todo => todo.id !== action.id);
        case 'TOGGLE':
          return todos.map(todo => todo.id === action.id ? 
              {...todo, checked : !todo.checked}:todo);
        default:
          return todos;
      }
    }
    
    
    function App() {
      //useState 에 데이터를 생성하는 함수를 대입할 때 
      //함수 호출 구문을 대입하면 데이터가 만들어 질 때 마다
      //리랜더링을 합니다.
      //함수 이름을 대입해야 함수를 전부 수행하고 1번만 리랜더링을 수행합니다.
      //const [todos, setToDos] = useState(createBulkTodos);
    
      //리듀서 설정 - 첫번째 매개변수는 호출될 함수
      //두번째는 매개변수는 초기값
      //세번째 매개변수는 호출할 메서드로 리턴하는 값이 초기값으로 설정됨
    
      //리턴될 결과는 state 이름 과 state를 수정할 함수
      const [todos, dispatch] = useReducer(
        todoReducer, undefined, createBulkTodos);
    
      //아이디를 위한 변수 생성
      const nextId = useRef(2001);
    
      //삽입을 처리하기 위한 함수
      //todos 에 변화가 생기면 함수를 만들지만 그렇지 않으면
      //기존 함수를 이용
      const onInsert = useCallback((text) => {
        const todo = {
          id:nextId.current,
          text,
          checked:false
        }
        //함수형 업데이트
        //setToDos(todos => todos.concat(todo));
    
        dispatch({type:'INSERT', todo});
        nextId.current += 1;
      }, []);
    
      //데이터 삭제를 위한 함수
      const onRemove = useCallback((id) => {
        //setToDos(todos=>todos.filter(todo => todo.id !== id));
        dispatch({type:'REMOVE', id});
      }, [])
    
      //데이터 수정을 위한 함수
      const onToggle = useCallback((id) => {
        //todos를 복제해서 하나씩 순회하면서
        //todo의 id 값 과 매개변수로 받은 id가 일치하면 
        //checked를 반전하고 그렇지 않으면 그대로
        //setToDos(todos => todos.map(todo => todo.id === id ?{...todo, checked:!todo.checked}:todo))
      
        dispatch({type:'TOGGLE', id});
      }, []);
    
      return (
        <ToDoTemplate>
          <ToDoInsert onInsert={onInsert}/>
          <ToDoList todos={todos} onRemove={onRemove} 
            onToggle={onToggle}/>
        </ToDoTemplate>
      );
    }
    
    export default App;


    > 컴포넌트 내부에서는 state를 수정하는 함수를 직접 생성하지 않음
    state 가 변경되더라도 함수들을 다시 만드는 작업을 하지 않음
    이 작업은 컴포넌트의 개수가 많거나 state를 조작하는 함수가 많을 때 수행 함
    함수를 만드는 작업은 그렇게 많은 자원을 소모하거나 많은 메모리를 사용하는 작업은 아님

    7) 화면에 보여질 내용만 랜더링

    > 스마트 폰 애플리케이션에서 CollectionView(Table View, Map View, Web View 등)들은 메모리 효율을 높이기 위해서 Deque 라는 자료구조를 이용해서 화면에 보여지는 만큼만 메모리 할당을 해서 출력을 하고 스크롤을 하면 Deque의 메모리를 재사용하는 메커니즘으로 메모리 효율을 높이게 됨

    > react 에서도 외부 라이브러리를 이용하면 위와 유사하게 화면에 보여지는 만큼만 랜더링을 하고 나머지 데이터를 스크롤을 할 때 랜더링을 하도록 할 수 있음.
    이는 SPA(Single Page Application)에서 많이 중요 이게 안될 때 어쩔 수 없이 서버 랜더링을 사용함

    > react-virtualized 라는 라이브러리를 이용해서 지연 로딩을 구현할 수 있음
    하나의 항목의 너비(ToDoList.scss 의 width : 512px) 와 높이(513px)를 알아야 하고 목록의 높이(57px)를 알아야 함

    💡 react 에서 목록을 출력할 때는 스타일에서 width 와 height를 설정하는 것이 좋음

    # 패키지 설치
    $yarn add react-virtualized

    # ToDoList.jsx를 수정
    import React, {useCallback} from "react";
    import ToDoListItem from "./ToDoListItem";
    import "./ToDoList.scss";
    
    import {List} from 'react-virtualized';
    
    const ToDoList = ({todos, onRemove, onToggle}) => {
        //하나의 항목을 랜더링하기 위한 함수를 생성
        const rowRenderer = useCallback(({index, key, style}) => {
            //출력할 데이터 가져오기
            const todo = todos[index];
            return(
                <ToDoListItem
                todo={todo}
                key={key}
                onRemove={onRemove}
                onToggle={onToggle}
                style={style}/>
            )
        }, [onRemove, onToggle, todos]);
    
        return(
            <List className="ToDoList"
            width={512} //항목의 너비
            height={513} //전체 높이
            rowCount={todos.length} //전체 데이터 개수
            rowHeight={57} //항목의 높이
            rowRenderer={rowRenderer} //행을 만들어주는 함수
            list={todos} //데이터
            style={{outline:'none'}} />
        )
    }
    export default ToDoList;​


    # ToDoListItem.jsx를 수정

    import React from 'react';
    
    import{
        MdCheckBoxOutlineBlank,
        MdCheckBox,
        MdRemoveCircleOutline
    } from 'react-icons/md';
    
    import cn from 'classnames';
    
    import "./ToDoListItem.scss";
    
    import { useCallback } from 'react';
    
    const ToDoListItem = ({todo, onRemove, onToggle, style}) => {
        //넘어온 데이터 중에서 text 와 checked 만 분해
        const {id, text, checked} = todo;
        //데이터 삭제 함수
        const onDelete = useCallback((e) => {
            const result = window.confirm(text + "를 정말로 삭제");
            if(result){
                onRemove(id);
            }
        }, [onRemove, id, text]);
    
        return(
            <div className="ToDoListItem-virtualized" 
                style={style}>
                <div className="ToDoListItem">
                    <div className={cn("checkbox", {checked})} 
                        onClick={(e)=>onToggle(id)}>
                        {checked ? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                        <div className='text'>{text}</div>
                    </div>
                    <div className="remove" onClick={onDelete}>
                        <MdRemoveCircleOutline />
                    </div>
                </div>
            </div>
        )
    }
    
    export default React.memo(ToDoListItem);​

     

    > 실행을 하면 데이터는 출력이 되는데 기본 모양이 망가짐

    # ToDoListItem.scss를 수정
    .ToDoListItem-virtualized{
        & + &{
            border-top:1px solid #dee2e6;
        }
        &:nth-child(even){
            background: #f8f9fa;
        }
    }
    
    .ToDoListItem{
        padding:1rem;
        display: flex;
        align-items:center;
        .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;
        }
    }​

    데이터 불변성

    1. 불변성

    > React 에서는 props 와 useState로 만든 데이터는 원본을 수정할 수 없음
    React 는 Virtual DOM 의 개념을 사용해서 랜더링을 구현
    React 는 현재 화면의 DOM 과 Memory 상의 Virtual DOM을 비교해서 수정된 부분만 다시 랜더링 하는 구조로 랜더링 속도를 향상 시킴
    게임의 물리 엔진도 이 원리를 이용함
    원본에 출력한 내용을 수정하면 안됨

    2. immer

    > 불변성을 신경쓰지 않으면서 업데이트 할 수 있는 라이브러리
    > immer 라이브러리의 produce 라는 함수를 이용해서 업데이트를 하면 객체를 복제해서 작업한 후 다시 원본에 적용을 해줌
    const state = {
    	number:1,
    }
    
    /*
    setState({...state, number:number + 1})
    */
    const nextState = produce(state, draft = >{
    	draft.number += 1;
    });
    
    //state를 복제해서 draft에 삽입을 하고 작업을 수행한 후 리턴
    //draft에 복제를 할 때 Deep Copy를 수행합니다.​

    3.immer를 사용하지 않고 불변성 유지 - 버튼을 누르면 등록을 하고 클릭하면 삭제

    # 프로젝트 생성 후 App.js 수정
    import React, {useRef, useCallback, useState} from 'react';
    
    function App() {
      //컴포넌트 안에서 사용할 변수 생성
      const nextId = useRef(1);
      
      //state(수정하면 리랜더링을 수행) 를 생성하고 setter 함수를 설정
      const [form, setForm] = useState({name:'', username:''});
      const [data, setData] = useState({
        array:[],
        uselessValue:null
      });
    
      //input에 입력받는 경우 입력하는 데이터가 변경될 때
      //state를 수정해주는 함수
      const onChange = useCallback((e) => {
        setForm({
          ...form,
          [e.target.name]:[e.target.value]
        })
      }, [form])
    
      //입력받은 데이터를 등록하는 함수
      //form에서 submit 이벤트가 발생할 때 호출
      //컴포넌트 안에서 함수를 만들 때는 특별한 경우가 아니면 
      //useCallback 안에 만드는 것이 좋습니다.
      //useCallback을 이용하면 두번째 매개변수로 대입된
      //deps 배열 안의 데이터가 변경되는 경우만 새로 만들어집니다.
      //useCallback을 사용하지 않으면 리랜더링 될 때 마다
      //함수가 다시 만들어집니다.
      const onSubmit = useCallback((e) => {
        e.preventDefault(); 
        //기본적으로 제공되는 이벤트를 수행하지 않도록 함
        //a 태그를 이용한 이동이나 form의 submit 이나 reset 이벤트는
        //화면 전체를 새로 생성합니다.
        //이전에 가지고 있던 내용을 모두 삭제합니다.
        //react, vue, angular는 SPA Framework 라서
        //화면 전체를 다시 랜더링하면 기본틀이 무너집니다.
        //화면에 출력된 내용과 가상의 DOM을 비교해서 변경된 부분만
        //리랜더링을 수행합니다.
        
        const info = {
          id:nextId.current,
          name:form.name,
          username:form.username
        }
    
        setData({
          ...data,
          array:data.array.concat(info)
        });
    
        setForm({
          name:'',
          username:''
        });
    
        nextId.current += 1;
      }, [data, form.name, form.username])
    
      //항목을 삭제하는 함수
      const onRemove = useCallback((e) => {
        setData({
          ...data,
          array:data.array.filter(info => info.id !== e)
        })
      }, [data]);
    
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
            name = "username"
            placeholder="아이디를 입력하세요"
            value={form.username}
            onChange = {onChange} />
            <input
            name = "name"
            placeholder="이름을 입력하세요"
            value={form.name}
            onChange = {onChange} />
            <button type="submit">등록</button>
          </form>
    
          <div>
            <ul>
              {data.array.map(info => (
                <li key={info.id} 
                  onClick={() => onRemove(info.id)}>
                    {info.username} ({info.name})
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;​

    4. immer를 이용한 불변성 유지

    # immer 설치
    $yarn add immer

    # App.js 파일에서 state를 수정하는 부분을 수정
    import React, {useRef, useCallback, useState} from 'react';
    
    import produce from 'immer';
    
    function App() {
      //컴포넌트 안에서 사용할 변수 생성
      const nextId = useRef(1);
      
      //state(수정하면 리랜더링을 수행) 를 생성하고 setter 함수를 설정
      const [form, setForm] = useState({name:'', username:''});
      const [data, setData] = useState({
        array:[],
        uselessValue:null
      });
    
      //input에 입력받는 경우 입력하는 데이터가 변경될 때
      //state를 수정해주는 함수
      /*
      const onChange = useCallback((e) => {
        setForm({
          ...form,
          [e.target.name]:[e.target.value]
        })
      }, [form])
      */
      const onChange = useCallback((e)=>{
        setForm(
          //draft 가 form의 복제본이 되고
          //draft를 수정하면 immer 가 알아서 
          //form에 데이터를 전송합니다.
          produce(draft => {
            draft[e.target.name] = e.target.value;
          })
        )
      }, [])
    
      //입력받은 데이터를 등록하는 함수
      //form에서 submit 이벤트가 발생할 때 호출
      //컴포넌트 안에서 함수를 만들 때는 특별한 경우가 아니면 
      //useCallback 안에 만드는 것이 좋습니다.
      //useCallback을 이용하면 두번째 매개변수로 대입된
      //deps 배열 안의 데이터가 변경되는 경우만 새로 만들어집니다.
      //useCallback을 사용하지 않으면 리랜더링 될 때 마다
      //함수가 다시 만들어집니다.
      /*
      const onSubmit = useCallback((e) => {
        e.preventDefault(); 
        //기본적으로 제공되는 이벤트를 수행하지 않도록 함
        //a 태그를 이용한 이동이나 form의 submit 이나 reset 이벤트는
        //화면 전체를 새로 생성합니다.
        //이전에 가지고 있던 내용을 모두 삭제합니다.
        //react, vue, angular는 SPA Framework 라서
        //화면 전체를 다시 랜더링하면 기본틀이 무너집니다.
        //화면에 출력된 내용과 가상의 DOM을 비교해서 변경된 부분만
        //리랜더링을 수행합니다.
        
        const info = {
          id:nextId.current,
          name:form.name,
          username:form.username
        }
    
        setData({
          ...data,
          array:data.array.concat(info)
        });
    
        setForm({
          name:'',
          username:''
        });
    
        nextId.current += 1;
      }, [data, form.name, form.username])
      */
      
      const onSubmit = useCallback((e) => {
        e.preventDefault(); 
    
        const info = {
          id:nextId.current,
          name:form.name,
          username:form.username
        }
        //data를 draft에 깊은 복사를 하고
        //draft에 작업을 수행한 후에 다시 data에 복제
        setData(produce(draft => {draft.array.push(info)}));
    
        setForm({
          name:'',
          username:''
        });
    
        nextId.current += 1;
    
      }, [form.name, form.username]);
    
    
    
    
      //항목을 삭제하는 함수
      /*
      const onRemove = useCallback((e) => {
        setData({
          ...data,
          array:data.array.filter(info => info.id !== e)
        })
      }, [data]);
      */
    
      const onRemove = useCallback((id) => {
        setData(produce(draft => 
          {draft.array.splice(
            draft.array.findIndex(info=> info.id === id),1)}
          ))
      }, []);
    
      return (
        <div>
          <form onSubmit={onSubmit}>
            <input
            name = "username"
            placeholder="아이디를 입력하세요"
            value={form.username}
            onChange = {onChange} />
            <input
            name = "name"
            placeholder="이름을 입력하세요"
            value={form.name}
            onChange = {onChange} />
            <button type="submit">등록</button>
          </form>
    
          <div>
            <ul>
              {data.array.map(info => (
                <li key={info.id} 
                  onClick={() => onRemove(info.id)}>
                    {info.username} ({info.name})
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    
    export default App;

    5. 운영 모드로 실행

    $yarn build
    $yarn global add serve(npm install --location=global serve)
    $serve -s build

     

Designed by Tistory.