TIL 22-04-25

thisisyjin·2022년 4월 25일
0

TIL 📝

목록 보기
30/113

React.js

Today I Learned ... react.js

🙋‍♂️ Reference Book

🙋‍ My Dev Blog


리액트를 다루는 기술 DAY 10

  • 일정 관리 App

To Do App

환경 구성

create-react-app

npm init후에,
yarn create react-app [프로젝트명]

패키지 설치

  1. sass
  2. classname
  3. react-icons

yarn add sass classname react-icons로 설치.

컴포넌트 구상

Component
TodoTemplate
TodoInsert
TodoList
TodoListItem

초기 코드 작성

1. .prettierrc 작성

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

2. index.css

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

3. App.js

const App = () => {
  return <div>Hello React!</div>
}

export default App;

컴포넌트 생성

/src/components 폴더에 생성.

1) 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;

props.children을 이용하여 하위 컴포넌트들을 렌더링함.

2) TodoTemplate.scss

.TodoTemplate {
  width: 512px;
  margin: 6rem auto 0;
  border-radius: 4px;
  overflow: hidden;

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

  .content {
    background: #fff;
  }
}

.TodoTemplate 안에 .app-title.content를 넣음.
= .TodoTemplate .app-title과 같음.

3) TodoList.js

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


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

export default TodoList;

4) 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;

react-icons 의 md(=Material Design icons)을 임포트함.

  • svg 파일을 컴포넌트처럼 사용 가능.
  • 크기 조절시 font-size 조절 or props 이용.

5) TodoList.scss

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

6) 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;
  }
}
  • nth-child() : 부모요소의 n번째 자식요소.
    -> even (짝수) / odd (홀수)
  • nth-of-type() : 부모요소의 n번째 타입이 일치하는 자식요소

  • flex : 1은 차지할 수 있는 영역을 모두 찾하라는 의미.
    -> flex-grow

Result

  • 컴포넌트 구성 + 디자인은 완료.
  • 이제, 실제로 React의 기능과 데이터를 구현해보자.

참고 - 파비콘은 테스트겸 등록해봤는데, 아래 코드처럼 작성함.

<link rel="icon" href="./fav.ico" type="image/x-icon" sizes="16x16" />

React 기능 구현

App.js

state를 갖게 함.

import { useState } from "react";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";
import TodoTemplate from "./components/TodoTemplate"

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라는 state를 갖게 한 후, (useState)
TodoList 컴포넌트에 props로 넘겨준다.

TodoList.js

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를 받아와서 각각의 todos 배열의 요소로 TodoListItem 컴포넌트를 생성함.
  • TodoListItem의 props로 todos 배열의 각 요소인 todo 를 넘겨줌.
    (todos는 배열 안에 요소로 객체가 있는 구조이므로, todo는 객체이다.)

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;
  • 마찬가지로 props.todo를 인자로 받고,
  • todo를 구조분해하여 text와 checked를 받아온다.
  • checked가 true면 <MdCheckBox />를 렌더링하고, false면 <MdRemoveCircleOutline />을 렌더링한다.
  • todo.text를 아래 div.text에 렌더링한다.
  • classnames 임포트
import cn from 'classnames';
  • cn함수 사용
<div className={cn('checkbox', {checked})}>

cn함수는 조건에 따라 클래스를 부여해줄 수 있다.
위 코드는 checkbox는 무조건 갖고, checked가 true면 checked라는 클래스도 갖게 한다.

  • 주의 ❗️
    반드시 cn함수 안에서 확정 클래스명은 ' '안에, 조건식이나 js코드는 { }안에 적어준다.

Result


항목 추가 기능 구현

  • TodoInsert를 이용하여 항목을 추가할 수 있도록.
  • state인 todos배열에 새로운 객체를 추가하는 로직 구현.

TodoInsert

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

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

    const onChangeInput = useCallback((e) => {
        setValue(e.target.value);
    }, []);

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

export default TodoInsert;
  • input 태그가 onChange시 실행되는 onChangeInput 함수를 정의하고, useCallback으로 감싸준다.
  • 함수 내에서 state를 참고하지 않으므로 한번만 저장되면 된다. (빈 배열[ ] 을 넣어준다.)
  • React Dev Tool로 TodoInsert를 살펴보면, input이 변할 때 마다 state가 달라진 값이 들어가게 된다.

App.js (setTodos) - useRef 이용

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

const App = () => {

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

  const nextId = useRef(4);

  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;
  • useRef를 이용하여 (로컬변수) index를 관리함. (todo 객체의 id값)
  • TodoInsert에 onInsert라는 함수를 props 로 넘겨줌.
  • onInsert 함수도 useCallback으로 감싸주고, 이 함수는 todos라는 state에 의존하는 함수이므로 두번째 인자로 [todos]를 적어줌.
    +) Array.prototype.concat은 새 배열을 반환하는 메서드임. (불변성)

TodoInsert (onSubmit)

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

const TodoInsert = ({onInsert}) => {
    const [value, setValue] = useState('');

    const onChangeInput = useCallback((e) => {
        setValue(e.target.value);
    }, []);

    const onSubmitForm = useCallback((e) => {
        onInsert(value);
        setValue('');

        e.preventDefault();
    }, [onInsert, value])

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

export default TodoInsert;
  • Form 제출시 발생하는 onSubmitForm 함수를 정의하고, useCallback으로 감싸줌.

  • 이 함수는 onInsert(props)와 value(state)에 의존하므로, 두번째 인자로 [onInsert, value]을 작성해줌.

  • form 제출시 발생하는 기본동작(=새로고침)을 막기 위해 e.preventDefault를 해줌.


삭제 기능 구현

  • 배열의 불변성을 지키면서, 배열 원소 제거시 -> filter 을 이용.
    (배열 원소 추가시에는 concat이나 spread(...)이용)

Array.prototype.filter()

  • 조건을 만족하는 (=true인) 모든 요소들을 모아 새 배열로 반환함.
  • 원본 불변성을 지킴.

App.js (setTodos)

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

const App = () => {

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

  const nextId = useRef(4);

  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;
  • onRemove 함수를 정의하여 useCallback으로 감쌈
  • 마찬가지로, todos(state)에 의존하므로 두번째 인자로 [todos]를 전달해줌.
  • todo 객체의 유일한 값인 id를 기준으로 필터링함.
  • todo.id와 인자로 받은 id가 일치하면 - 필터링되게.
    (따라서 조건에는 !==를 적어줘야 나머지가 false가 되어 배열의 요소로 들어감)
  • TodoList 컴포넌트로 전달해주고, TodoList는 각각의 TodoListItem에 onRemove를 넘겨줘야함.
<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />

TodoListItem.js (onClick)

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;
  • onRemove(id)는 id라는 인자가 들어가므로, 그냥
    onClick={onRemove(id)}로 작성하면 에러가 발생한다.
  • 반드시 화살표함수의 리턴값으로 함수를 넣어줘야한다.
<div className='remove' onClick={() => onRemove(id)}>

Result


체크 기능 구현

App.js (setTodos)

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

const App = () => {

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

  const nextId = useRef(4);

  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) => {
    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;

onToggle 로직

  • id가 일치하는 todo를 골라서 todo.checked를 토글함.
 const onToggle = useCallback((id) => {
    setTodos(todos.map(todo => todo.id === id? {...todo, checked: !todo.checked} : todo))
  }, [todos])

TodoListItem

  • 마찬가지로 TodoList에서 TodoListItem으로 props로 넘겨준다.
  • TodoListItem에서 props로 가져오고, onClick에 넣어준다.
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;
  • 반드시 화살표 함수onClick={() => onToggle(id)}와 같이 적어줘야한다.

Final Result

1. insert
2. delete
3. toggle

  • todos는 App에 존재하는 state이므로, onInsert, onDelete, onToggle 함수 모두 App.js에 선언해준다.
  • todos의 요소인 todo를 구분할 수 있는 것은 todo.id 이다.
  • todoList에서 todos.map( todo => ... )에서 클릭한 것이 어떤 컴포넌트인지, id가 무엇인지 알 수 있다. (중간다리 역할)

  • todoListItem에서는 직접 click이 발생한다. (onClick을 실제로 달아주고, onDelete과 onToggle을 인자와 함께 넣어줌)


    KEY POINT

    1. todos (state)는 App.js에서 관리 - useState(초기값)
    2. onInsert, onRemove, onToggle함수 정의도 App.js에서.
      -> 전부 App의 state인 todos를 참조하기 때문.
    3. TodoInsert에는 onInsert를,
      TodoList에는 (->TodoListItem) onRemove, onToggle을 props로 넘김
    4. TodoInsert는 onChangeInput과 onSubmitForm이 존재.
    5. TodoInsert는 value라는 state가 존재. = Input의 value
      onSubmitForm은 onInsert함수와 value에 의존함.
    6. TodoList는 todos.map(todo => ...)하여 각 요소들을 TodoListItem으로 렌더링함.
      -> todo, onToggle을 props로 넘김. (todo는 자기 자신, 즉 todos의 각 요소. = 객체 자체)
    7. TodoListItem 에서는 classname를 사용하여 todo.checked 여부에 따라 클래스명 부여
    8. todo.id, todo.text, todo.checked를 이용하여 렌더링.
    9. div.checkbox 클릭시 - onToggle(id)
    10. div.remove 클릭시 - onRemove(id)
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글