React/[책] 리액트를 다루는 기술

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

spring_sunshine 2022. 4. 15. 23:22

10.1 프로젝트 준비하기 

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

 

create-react-app을 사용하여 프로젝트를 생성하자.

$ yarn create react-app todo-app

프로젝트가 생성되면 todo-app 디렉터리로 들어가서 yarn을 사용하여 필요한 라이브러리를 설치하자.

$ cd todo-app
$ yarn add node-sass@4.14.1 classnames react-icons

 

2) Prettier 설정 

 

.prettierrc

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

 

3) index.css 수정 

 

index.css

body {
  margin:0;
  padding:0;
  background: #e9ecef;  // background 색상 변경
}

 

4) App 컴포넌트 초기화 

 

이제 기존에 있던 App 컴포넌트의 내용을 모두 삭제하자.

 

App.js

import React from 'react';
const App = () => {
	return <div>Todo App을 만들자!</div>
};

export default App;

 

 


10.2 UI 구성하기 

TodoTemplate

화면을 가운데에 정렬고, 앱 타이틀(일정 관리)을 보여준다. children으로 내부 JSX를 props로 받아 와서 렌더링한다. 

 

TodoInsert

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

 

TodoListItem 

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

 

TodoList

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

 

→ 총 네 개의 컴포넌트를 만들고, 이 컴포넌트들은 src 디렉터리에 components라는 디렉터리를 생성해서 그 안에 저장한다. (기능상 필요하기 때문이 아니라 자주 사용되는 관습임)

 

 

1) TodoTemplate

src 디렉터리에 components 디렉터리를 생성한 뒤 그 안에 TodoTemplate.js와 TodoTemplate.scss 파일을 생성하자.

 

TodoTemplate.js

import React from 'react';
import './TodoTemplate.scss'

const TodoTemplate=({children})=>{
    return(
        <div className="TodoTemplate">
            <div className="app-title">일정 관리</div>
            <div className="content">{children}</div>
        </div>
    );
};

export default TodoTemplate;

App.js

import React from 'react';
import TodoTemplate from './components/TodoTemplate';

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

export default App;
  • 이 컴포넌트를 작성하는 과정에서 다음과 같이 상단에 import를 넣지 않고 바로 컴포넌트를 사용하려고 하면, VS Code 에디터에서 자동 완성 기능이 나타날 것이다. 
  • TodoTemplate.js 컴포넌트가 VS Code에서 다른 탭으로 열려 있지 않으면 자동 완성이 작동하지 않으므로 닫혀 있는 파일에도 자동 완성이 제대로 작동하려면 프로젝트 최상위 디렉터리에 jsconfig.json 파일을 만들어야 한다. 

jsconfig.json

{
    "compilerOptions": {
        "target": "es6"
    }
}

 

TodoTemplate.scss

.TodoTemplate{
    width:512px;
    margin-left:auto;
    margin-right:auto;
    margin-top:6rem;
    margin-top: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;
    }
}
더보기

Module Error (from ./node_modules/sass-loader/dist/cjs.js): Node Sass does not yet support your current environment: Windows 64-bit with Unsupported runtime (93)

갑자기 프로젝트를 다시 시작해보니까 node sass와 nodejs의 버전 호환 문제 때문에 이런 시련이 주어졌다..

그치만 어쩌다 해결!

  • overflow: hidden
    • 콘텐츠를 안쪽 여백 상자에 맞추기 위해 잘라낸다. 스크롤바를 제공하지 않는다. (visible이 기본값)
  • display: flex;

https://studiomeal.com/archives/197

 

이번에야말로 CSS Flex를 익혀보자

이 튜토리얼은 “차세대 CSS 레이아웃” 시리즈의 첫번째 포스트입니다. 이번에야말로 CSS Flex를 익혀보자 이번에야말로 CSS Grid를 익혀보자 벌써부터 스크롤의 압박이 느껴지고,‘좀 편안하게 누

studiomeal.com

flexbox란?

부모 요소인 flex container와 자식 요소인 flex item으로 구성된다.

flex container가 flex 태그의 영향을 받는 공간이고, 그 안에서 flex item들의 배치, 정렬이 설정된 속성에 따라 변경되는 것이다. 

flex container에 "display:flex"를 적용하면 flexbox 속성을 사용할 수 있다.

 

 

div는 기본적으로 block 속성을 갖기 때문에 수직으로 차곡 차곡 쌓이게 된다. 

기존

 

flex 설정 후

위와 같이 display 속성값을 설정하면서 해당 div가 flex container이며 내부 div들이 flex item임을 정의하였다. 

 

 

2) TodoInsert  

  • 할 일 항목을 추가하는 컴포넌트
  • form 태그로 인풋태그, 버튼태그를 감쌌다. 

TodoInsert.js

// 새 항목을 입력하고 추가하는 컴포넌트
// state를 통해 인풋상태를 관리
import React from 'react';
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;
  • form 태그: 입력 양식 전체를 감싸는 태그로 그 안에 포함된 복수의 input, select, textarea 태그를 컨트롤 하는 역할을 한다. 
  • 리액트에선 보통 state로 form의 value를 관리하고, submit 이벤트로 원하는 동작을 실행한다. 

 

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;
    //input에 글쓰면 하얀색
    &::placeholder{
        color:#dee2e6;
    }
    // 버튼을 제외한 영역을 모두 차지하기
    flex:1;
    }

    button{
        // 기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        background: #868e96;
        color: white;
        padding: 0rem 1rem;
        font-size: 1.5rem;
        display: flex;
        align-items: center;
        cursor: pointer;
        transition: 0.1s #868e96 ease-in;
        &:hover{
            background: #adb5ad;
        }
    }
}

 

3) TodoListItem과 TodoList 

이제 일정 관리 항목이 보이게 해 줄 TodoListitem과 TodoList를 만들 차례이다. 

TodoListItem: 항목 컴포넌트 UI

TodoList: TodoListItme을 렌더링하는 컴포넌트

 

TodoListItem.js

// 각 할 일 항목에 대한 정보를 보여주는 컴포넌트
// todo 객체를 props로 받아와서 상태에 따라 다른 UI를 보여줌
import React from 'react';
import { MdCheckBoxOutlineBlank, 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;
  • 다양한 아이콘을 불러와 사용했고, 아직 MdCheckBox 아이콘 컴포넌트는 사용하지 않은 상태이다. 

 

TodoList.js

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

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

export default TodoList;
  • 컴포넌트에 TodoListItem을 불러와서 별도의 props 전달 없이 그대로 여러 번 보여 주고 있다.

 

그 다음으론 스타일을 지정해보자.

 

TodoList.scss

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

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 ;
    }
}
  • &문자를 사용하여 Sass처럼 자기 자신 선택이 가능하다. 

 

 

 


10.3 기능 구현하기 

1) App에서 todos 상태 사용하기

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

App.js

import React,{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;
  • todos 배열 안에 들어 있는 객체에는 각 항목의 고유 id, 내용, 완료 여부를 알려 주는 값이 포함되어 있다.
  • 이 배열은 TodoList에 props로 전달이 되고, TodoList에서 이 값을 받아 온 후 TodoItem으로 변환하여 렌더링하도록 설정해야 한다.

TodoList.js 

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

const TodoList = ({ todos }) => {
	return (
    	<div className = "TodoList">
        	{todos.map(todo => (
            	<TodoListItem todo={todo} key={todo.id} />
            ))}
        </div>
    );
};
  • props로 받아 온 todos 배열을 배열 내장함수 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링해 주었다. (map을 사용하여 컴포넌트로 변환할 때는 key props를 전달해 주어야 함)
  • todo 데이터는 통째로 props를 전달해주자.
  • map() 사용법: arr.map(callback , [thisArg])

TodoListItem.js

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

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 컴포넌트는 App에서 전달해 준 todos 값에 따라 다른 내용을 제대로 보여 준다. 

 

classnames는 조건부로 classNames를 결합할 수 있는 JS 유틸리티이다. 

더보기

classNames는 React 라이브러리중 하나이다.
className을 적용할 때 다양한 옵션을 붙여서 className을 적용할 수 있다.
className에 true에 해당하는 값은 추가로 표시 가능하고, false인 값은 className에 적용되지 않는다.

<div className={classNames()} />	// 기본형태이다.

classNames를 어떻게 사용하는지는 아래와 같다.

classNames('test')			// 'test'

classNames('test', 'box')		// 'test box'

classNames('test', {box: false})	// 'test'

classNames('test', {box: true})		// 'test box'

classNames({test: true, box: true})	// 'test box'

classNames(['test', 'box'])		// 'test box'

classNames(null, false, undefined, 0, '')	// ''

맨 아래 classNames처럼 2개 이상 받을 수도 있다.
따로 false 값을 적용하지 않는 이상 값을 받으면 true

 

<div className={cn('checkbox', { checked })}>
        {checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}

 

→ 'checkbox'는 조건 없이 기본으로 추가하고, checked 값이 true에 해당하는 값은 추가로 표시할 수 있고, false이면 className에 적용되지 않는다. 

true: checkbox checked / false: checkbox

 

2) 항목 추가 기능 구현하기

  • 일정 항목을 추가하는 기능을 구현해보자. 
  • 이 기능을 구현하려면 TodoInsert 컴포넌트에서 인풋 상태를 관리하고, App 컴포넌트에는 todos 배열에 새로운 객체를 추가하는 함수를 만들어야 한다.
  • TodoInsert 컴포넌트에서 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의하고, 추가로 인풋에 넣어 줄 onChange 함수도 작성해야 한다.
  • 인풋에 value랑 onChange를 설정하지 않아도 입력이 가능하지만, 리액트 컴포넌트에서 해당 인풋에 무엇이 입력되어 있는지 추적하지 않는다. 
  • 이 과정에서 컴포넌트가 리렌더링될 때마다 함수를 새로 만드는 것이 아니라, 한 번 함수를 만들고 재사용할 수 있도록 useCallback Hook을 사용하는 것이다.

https://enfanthoon.tistory.com/133

 

[React] - 13) onSubmit/onClick의 차이

onSubmit / onClick 리액트에서 이벤트를 처리할 때, 그동안은 보통 onClick을 사용했습니다. 하지만 리액트에서는 종종 onSubmit을 사용하기도 합니다. 한번 그 차이를 알아볼까요? 우선 onClick으로 구현

enfanthoon.tistory.com

 

 

 

TodoInsert.js

import React, { useCallback ,useState} 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" onSubmit={onSubmit}>
            <input placeholder="할 일을 입력하세요"
            value={value}
            onChange={onChange}
            />
            <button type="submit">
                <MdAdd/>
            </button>
        </form>
    )
}
export default TodoInsert;

 

 

App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만들어 보자.

새로운 객체를 만들때마다 id값에 1을 더해주어야 하는데, id 값은 useRef를 사용해서 관리해보자. 

이 값은 화면에 렌더링되는 정보도 아니고, 이 값이 바뀐다고 해서 컴포넌트가 리렌더링될 필요도 없기 때문이다. 

더보기

useRef를 활용한 변수 예시

  • setTimeout, setInterval을 통해 만들어진 id
  • scroll 위치
  • 배열에 새 항목을 추가할 때 필요한 고유값 key
  • 포커스, 선택영역, 혹은 미디어의 재생을 관리할 때 주로 사용 

useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킨다. 

또, 렌더링과 상관없이 바뀔 수 있는 값인 컴포넌트 로컬 변수를 사용할 때도 쓰일 수 있다. 

...
const nextId = useRef(4);
const onInsert = useCallback(
	text => {
    	const newTodo = {
            id: nextId.current,
            text,
            checked: false,
    	};
        nextId.current += nextId.current+1;
        
        setTodos(todos.concat(newTodo));
    },
    [todos],
);

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

 

TodoInsert에서는 onSubmit 이벤트를 설정해서 이 함수가 호출되면 props로 받아온 onInsert 함수에 현재 value값을 넣어서 호출하고, 현재 value값을 초기화 한다. 또 onSubmit 이벤트는 브라우저를 새로고침 하므로 유의하자.

const onSubmit = useCallback(
	e => {
    	onInsert(value);
        setValue('');
    	e.preventDefault(); // 이 부분이 없으면 새로고침 되면서 데이터 저장이 안됨. 
    }
    ,[onInsert, value],
);

→ [onInsert, value] 를 넣어서 onInsert함수와 value가 바뀔 때 함수를 새로 생성하기를 명시함. 
→ 함수 내부에서 상태 값에 의존해야 할 때는 반드시 두번째 파라미터 안에 그 값을 포함시켜야 한다.

위 함수에선 두번째 파라미터 설정이 매우매우 중요하였다. value를 넣어주지 않으면 새로운 항목에 value가 기입되지 않는다. 

 

useCallback( )
useCallback을 사용할 때
1. 첫번째 인자에 콜백함수
2. 두번째 인자에 배열
3. 배열 안에는 state 또는 props으로 받고 있는 의존성 값을 넣어줘야 한다. (중요)
→ 콜백 안에서 참조되는 모든 값은 의존성 값의 배열에 나타나야 한다 !!!

 

 

 

이제, onRemove 함수를 구현해봅시다. 배열에 있는 항목을 제거할 때에는, 추가할떄와 마찬가지로 불변성을 지켜가면서 업데이트를 해주어야 합니다.

불변성을 지키면서 특정 원소를 배열에서 제거하기 위해서는 filter 배열 내장 함수를 사용하는것이 가장 편합니다. 이 함수는 배열에서 특정 조건이 만족하는 원소들만 추출하여 새로운 배열을 만들어줍니다. 

 

3) 지우기 기능 구현

  • 배열에 있는 항목을 제거할 때에는, 추가할 때와 마찬가지로 불변성을 지켜가면서 업데이트를 해줘야 한다. 
  • 불변성을 지키면서 특정 원소를 배열에서 제거하기 위해서는 filter 배열 내장 함수를 사용하는것이 가장 편하다. 이 함수는 배열에서 특정 조건이 만족하는 원소들만 추출하여 새로운 배열을 만든다.

App.js

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

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

export default App;

 

 

4) 체크 기능 구현

  • onToggle이라는 함수를 App에 만들고, 해당 함수를 TodoList 컴포넌트에게 props로 넣어준다.
  • 그 다음에 TodoList를 통해 TodoListItem까지 전달해주면 된다. 

<내 틀린 코드>

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

 

spread 연산자

✅ 객체 복사 또는 업데이트

객체에서 spread operator를 이용하여 객체의 복사 또는 프로퍼티를 업데이트 할 수 있습니다. 

간단한 State Management 구현을 위해서 다음과 같은 방식으로 응용하여 사용하기도 합니다. 

var currentState = { name: '철수', species: 'human'};
currentState = { ...currentState, age: 10}; 

console.log(currentState)// {name: "철수", species: "human", age: 10}

currentState = { ...currentState, name: '영희', age: 11}; 
console.log(currentState); // {name: "영희", species: "human", age: 11}

https://paperblock.tistory.com/62

 

[ES6] Spread Operator (스프레드 연산자)

ES6에서는 '...'와 같이 다소 특이한 형태의 문법이 추가되었습니다. 점 3개가 연달아 붙어있는 이 표시는 Spread Opertor(스프레드 오퍼레이터, 스프레드 연산자, 전개 구문, 펼침 연산자...)를 나타내

paperblock.tistory.com

 

 

App.js

import React,{useCallback, useState, useRef} from 'react';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
import TodoTemplate from './components/TodoTemplate';
  
const App = () => {
	(...)
  
	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;

 


10.4 정리 

  1. 이번에 만든 프로젝트는 소규모이기 때문에 따로 컴포넌트 리렌더링 최적화 작업을 하지 않아도 정상적으로 작동한다. 
  2. 하지만 일정 항목이 몇 만개씩 생긴다면 새로운 항목을 추가하거나 기존 항목을 삭제 및 토글할 때 지연이 발생할 수 있다.
  3. 따라서 클라이언트 자원을 더욱 효율적으로 사용하려면 불필요한 리렌더링을 방지해야 하고, 이 내용은 11장에서 다뤄보자.