Today I Learned ... react.js
🙋♂️ Reference Book
🙋 My Dev Blog
리액트를 다루는 기술 DAY 10
- 일정 관리 App
npm init후에,
yarn create react-app [프로젝트명]
yarn add sass classname react-icons로 설치.
| Component | 
|---|
| TodoTemplate | 
| TodoInsert | 
| TodoList | 
| TodoListItem | 
.prettierrc 작성{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all",
    "printWidth": 80
}body {
  margin: 0;
  padding: 0;
  background-color: #e9ecef;
}const App = () => {
  return <div>Hello React!</div>
}
export default App;/src/components 폴더에 생성.
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을 이용하여 하위 컴포넌트들을 렌더링함.
.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과 같음.
import TodoListItem from "./TodoListItem";
import './TodoList.scss';
const TodoList = () => {
    return (
        <div className="TodoList">
            <TodoListItem />
            <TodoListItem />
            <TodoListItem />
        </div>
    );
};
export default TodoList;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 이용.
.TodoList {
  min-height: 320px;
  max-height: 513px;
  overflow-y: auto;
}.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

참고 - 파비콘은 테스트겸 등록해봤는데, 아래 코드처럼 작성함.
<link rel="icon" href="./fav.ico" type="image/x-icon" sizes="16x16" />
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로 넘겨준다.
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 컴포넌트를 생성함.todo 를 넘겨줌.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를 인자로 받고, <MdCheckBox />를 렌더링하고, false면 <MdRemoveCircleOutline />을 렌더링한다.
- classnames 임포트
import cn from 'classnames';
- cn함수 사용
<div className={cn('checkbox', {checked})}>cn함수는 조건에 따라 클래스를 부여해줄 수 있다.
위 코드는 checkbox는 무조건 갖고, checked가 true면 checked라는 클래스도 갖게 한다.
- 주의 ❗️
반드시 cn함수 안에서 확정 클래스명은 ' '안에, 조건식이나 js코드는 { }안에 적어준다.

todos배열에 새로운 객체를 추가하는 로직 구현.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;
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;props 로 넘겨줌.todos라는 state에 의존하는 함수이므로 두번째 인자로 [todos]를 적어줌.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 을 이용.Array.prototype.filter()
- 조건을 만족하는 (=true인) 모든 요소들을 모아 새 배열로 반환함.
- 원본 불변성을 지킴.
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;state)에 의존하므로 두번째 인자로 [todos]를 전달해줌.todo.id와 인자로 받은 id가 일치하면 - 필터링되게.!==를 적어줘야 나머지가 false가 되어 배열의 요소로 들어감)<TodoListItem todo={todo} key={todo.id} onRemove={onRemove} />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;onClick={onRemove(id)}로 작성하면 에러가 발생한다.<div className='remove' onClick={() => onRemove(id)}>
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])
props로 넘겨준다.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)}와 같이 적어줘야한다.
1. insert
2. delete
3. toggle
todoList에서 todos.map( todo => ... )에서 클릭한 것이 어떤 컴포넌트인지, id가 무엇인지 알 수 있다. (중간다리 역할)
todoListItem에서는 직접 click이 발생한다. (onClick을 실제로 달아주고, onDelete과 onToggle을 인자와 함께 넣어줌)
KEY POINT
- todos (
state)는 App.js에서 관리 - useState(초기값)- onInsert, onRemove, onToggle함수 정의도 App.js에서.
-> 전부 App의 state인 todos를 참조하기 때문.- TodoInsert에는 onInsert를,
TodoList에는 (->TodoListItem) onRemove, onToggle을props로 넘김- TodoInsert는 onChangeInput과 onSubmitForm이 존재.
- TodoInsert는
value라는 state가 존재. = Input의 value
onSubmitForm은 onInsert함수와 value에 의존함.- TodoList는 todos.map(todo => ...)하여 각 요소들을 TodoListItem으로 렌더링함.
-> todo, onToggle을props로 넘김. (todo는 자기 자신, 즉 todos의 각 요소. = 객체 자체)- TodoListItem 에서는 classname를 사용하여 todo.checked 여부에 따라 클래스명 부여
- todo.id, todo.text, todo.checked를 이용하여 렌더링.
div.checkbox클릭시 - onToggle(id)
div.remove클릭시 - onRemove(id)