다라다라V
article thumbnail
728x90
반응형

우리는 이전의 단원들을 통해 리액트의 기본기부터 컴포넌트를 스타일링하는 방법까지를 배웠습니다. 지금까지 배운 내용을 활용하여 프런트 엔드를 공부할 때 자주 구현하는 일정관리 애플리케이션을 만들어보겠습니다.

이번 실습은 다음과 같은 순서로 진행됩니다.

  1. 프로젝트 준비하기
  2. UI 구성하기
  3. 기능 구현하기

📌 프로젝트 준비하기

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

일정 관리 애플리케이션을 만들기위해 터미널에 create-react-app을 사용한 명령어를 입력하겠습니다. 또한 프로젝트가 생성되면 todo-app 디렉터리로 이동하여 애플리케이션을 만들 때 필요한 라이브러리를 설치하세요.

$ yarn create react-app todo-app
$ cd todo-app
$ yarn add node-sass classnames react-icons
  • sass : Sass 를 사용할 예정이므로 설치
  • classnames : 조건부 스타일링을 편하게 하기 위해 설치
  • react-icons : 리액트에서 다양하고 예쁜 아이콘을 사용하는 라이브러리, 아이콘을 컴포넌트처럼 다룰 수 있음

 

📚 Prettier 설정 

  • Prettier : 코드가 제대로 정렬되고, 세미콜론(;)이 바진 곳에 자동 추가되는 등의 자동 코드 정리를 해줌, .prettierrc라는 파일에서 이러한 스타일링을 커스터마이징할 수 있음

Prettier을 설정하여 코드 스타일링을 깔끔하게 정리합시다. .prettierrc 파일을 다음과 같이 만드세요.

// .prettierrc

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

 

📚 index.ccss 수정 

글로벌 스타일 파일이 들어있는 index.css를 수정하겠습니다.

// index.css

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

 

📚 App 컴포넌트 초기화

컴포넌트를 초기화 한 뒤, 프로젝트 디렉터리에서 yarn start 명령어를 입력하여 개발 서버를 구동하세요.

// App.js

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


📌 UI 구성하기

앞으로 만들 컴포넌트들은 다음과 같습니다. 이 컴포넌트들은 관습적으로 src 디렉터리에 components 디렉터리를 만들어 그 안에 저장합니다.

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

 

📚 TodoTemplate 만들기

// TodoTemplate.js

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를 따로 해야합니다.

// App.js

import React from 'react';
import TodoTemplate from './components/TodoTemplate';
 
const App = () => {
  return <TodoTemplate>Todo App을 만들자!</TodoTemplate>;
};
 
export default App;

 

TodoTemplate의 css 스타일을 작성하기 위한 코드는 다음과 같습니다.

// TodoTemplate.scss

.TodoTemplate {
    width: 512px;
    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;
    }
}
  • flex는 grid 이전의 레이아웃 배치 전용 기능으로 고안되었습니다.
  • 가로 방향으로 아이템들이 배치되고 높이는 컨테이너 크기로 자동 조정됩니다.
  • flex라는 display 속성을 더 자세히 알고 싶다면 Flexbox Froggy(https://flexboxfroggy.com/#ko)에 방문하여 학습하는 것도 좋은 방법입니다.


📚 TodoInsert 만들기

// TodoInsert.js

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;
import { 아이콘 이름 } from 'react-icons/md'

App.js에도 렌더링합니다.

// App.js

import TodoInsert from './components/TodoInsert';
import TodoTemplate from './components/TodoTemplate';
 
const App = () => {
  return(
    <TodoTemplate>
      <TodoInsert/>
    </TodoTemplate>
  )
};
 
export default App;

스타일링은 다음과 같습니다.

// 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;
    }
  }
}
  • + 버튼에 마우스를 올리면 버튼의 배경색상이 바뀌는 것을 확인할 수 있습니다.


📚 TodoListItem 과 TodoList 만들기

일정 관리 항목을 보일 TodoListItem과 TodoList 컴포넌트를 만들어 봅시다.


// TodoListItem.js

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;
  • 아직 사용하지 않은 아이콘인 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;
  }
}

 

컴포넌트 스타일링을 마친 후에는 App.js에 만들어둔 컴포넌트를 모두 랜더링해봅시다.

// App.js

import React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/TodoInsert';
import TodoList from './components/TodoList';
 
const App = () => {
  return (
    <TodoTemplate>
      <TodoInsert />
      <TodoList />
    </TodoTemplate>
  );
};
 
export default App;


📌 기능 구현하기

📚 App 에서 todos 상태 사용하기

// 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: 'DaraDaraV 구독하기',
      checked: true,
    },
    {
      id: 2,
      text: '티스토리 좋아요 누르기',
      checked: true,
    },
    {
      id: 3,
      text: '댓글 남기기',
      checked: false,
    },
  ]);

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

export default App;
  • 추가한 일정 항목에 대한 상태들은 App 컴포넌트에서 관리합니다. useState를 사용하여 todos라는 상태를 정의하고 TodoList의 props로 전달하겠습니다.
  • todos 배열 안에 들어있는 객체는 고유의 id, 내용, 완료 여부를 알려주는 값을 가집니다.
  • 이 배열은 TodoList 에 props 로 전달됩니다.

 

// 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>
  );
};
 
export default TodoList;
  • props로 받아온 todos 배열은 map을 통해 TodoListItem 으로 이루어진 배열로 변환해 렌더링했습니다.
  • map 을 사용할 때는 key props를 전달해야하는데 여기서는 id 값을 사용했습니다.

 

TodoListItem 컴포넌트에서 받아온 todo 값에 따른 UI를 보여주도록 코드를 수정해봅시다.

// TodoListItem.js

import React from 'react';
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;


📚 항목 추가 기능 구현하기

일정항목을 추가하기 위해서는 TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어야합니다.

 

📃 TodoInsert value 상태 관리하기

// TodoInsert.js

import React, { 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;
  • 인풋에 입력하는 값을 관리할 수 있도록 useState를 사용하여 value라는 상태를 정의합니다
  • 인풋에 넣어줄 onChange 함수도 작성하되, 함수를 재사용하도록 useCallback Hook을 사용합니다.

 

state가 잘 업데이트 되었는지를 확인하는 다른 방법은 리액트 개발자 도구를 사용하는 방법이 있습니다.

https://chrome.google.com/webstore/category/extensions

  • onChange 함수에서 console.log 함수를 찍어보는 방법을 이용하면 됩니다.
  • 위의 주소에서 리액트 개발자 도구를 설치후 열면 개발자 도구 탭에 Component가 나타납니다.
  • TodoInsert를 선택하면 인풋을 수정했을 때 Hooks의 State 부분이 변경되는 것을 확인할 수 있습니다.

 

📃 todos 배열에 새 객체 추가하기

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

const App = () => {
 ( ... )

  // 고윳값으로 사용될 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;
  • App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수입니다.
  • 새로운 객체를 만들 때 마다 id 값에 1을 더해야하는데 useRef를 이용해 이를 관리합니다.
  • id 값은 렌더링되는 값이 아니기 때문에 useState가 아닌 useRef를 이용합니다.
  • 또한 onInsert 함수는 컴포넌트 성능을 위해 useCallback 으로 감쌉니다.

 

📃 TodoInsert에서 onSubmit 이벤트 설정하기

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(''); // 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;
  • App에서 TodoInsert에 넣어둔 onInsert 함수에 현재 useState로 관리하는 value 값을 파라미터로 넣어 호풀합니다.
  • onSubmit 함수가 호출되면 props로 받아온 onInsert 함수에 현재 value 값을 파라미터로 넣어 호출하고, 현재 value 값을 초기화합니다.
  • onSubmit 이벤트는 또한 브라우저를 새로 고침하기에 e.preventDefault() 함수를 이용해 새로고침을 방지합니다.


📚 지우기 기능 구현하기

리액트 컴포넌트에서는 배열의 불변성을 지키면서 배열 원소를 지워야하는 경우, 배열의 내장함수 filter를 사용합니다.

 

📃 배열 내장 함수 filter

filter 함수는 기존의 배열을 그대로 둔 상태에서 특정 조건을 만족하는 원소들만 추출하여 새로운 배열을 만듭니다.

const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const biggerThanFive = array.filter(number => number > 5);
// result: [6, 7, 8, 9, 10]
  • 함수의 파라미터로 조건문을 넣어둡니다.
  • 파라미터로 넣는 함수는 ture 혹은 flase 값을 반환해야합니다.
  • true를 반환하는 원소만 새로운 배열에 포함됩니다.

 

📃 todos 배열에서 id로 항목 지우기

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 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;
  • App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수 onRemove
  • TodoList의 props를 설정해야함

 

📃 TodoListItem에서 삭제 함수 호출하기

// TodoList.js

import React from ‘react‘;
import TodoListItem from ‘./TodoListItem‘;
import ‘./TodoList.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에서 onRemove 함수를 사용하기 위해 TodoList 컴포넌트를 거쳐야합니다.
  • props로 받아온 onRemove 함수를 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 }) => {
  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;
  • 삭제 버튼을 누르면 TodoListeItem에서 onRemove 함수에 현재 자신이 가진  id를 넣어서 삭제 함수를 호출합니다.

📚 수정 기능

수정 기능은 삭제 기능과 비슷합니다.

onToggle이라는 함수를 App에 만들고 해당 함수를 TodoList 컴포넌트에 props로 넣어줍니다. 이 함수는 TodoList를 통해 TodoListItem에게 전달합니다.

📃 onToggle 구현하기

// 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 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;
  • 배열 내장 함수인 map을 이용하여 특정 id 객체의 checked 값을 반전시킵니다.
  • 불변성을 유지하면서 특정 배열 원소를 업데이트할 때는 map을 사용합니다.
  • map을 사용하여 만든 배열에서 변화가 필요한 원소만 업데이트 되고 나머지는 그대로 있습니다.

 

📃 TodoListItem 에서 토글 함수 호출하기

App에서 만든 onToggle 함수를 TodoListItem에서 호출하도록 TodoList를 거쳐 전달합시다.

// 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;

 

// 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;

 


📌 QUIZ

  1. react에서 아이콘을 컴포넌트처럼 사용할 수 있도록 하는 라이브러리의 이름은 (____) 입니다.
  2. (____) 는 코드를 작성할 때 코드 스타일을 깔끔하게 정리하는데 사용합니다.
  3. (____) 라는 display 속성은 배치에 사용됩니다.
  4. map 을 사용하여 컴포넌트로 변환할 때는 (____) 를 전달해야하기에 실습에서는 id 값을 넣어주었습니다.
  5. (____) 는 브라우저에 나온 리액트 컴포넌트를 심층 분석할 수 있도록 리액트 개발 팀이 만든 도구 입니다.
  6. 리액트 배열에서 특정 조건만을 만족하는 원소만을 추출하기 위한 함수는 (____) 입니다.
  7. (____) 함수를 이용하면 배열의 특정 원소만 업데이트하여 사용할 수 있습니다.
  8. react-icons의 아이콘 "MdAdd" 를 사용하기 위한 코드를 작성해보세요
  9. 우리가 진행한 실습에서는 TodoInsert에서 onSubmit 이벤트 설정하였습니다. onClick 이벤트로 우리가 했던 이벤트 처리를 하는 코드를 작성해보세요.

 

  1. react-icons
  2. Prettier
  3. flex
  4. key props
  5. 리액트 개발자 도구
  6. filter
  7. map
  8.  
import { MdAdd } from 'react-icons/md'

    9. 

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

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

 

 

 

 

 

반응형
profile

다라다라V

@DaraDaraV

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!