[스터디]리액트를 다루는 기술-일정관리 웹 어플리케이션
아래 명령어를 이용해 새로운 프로젝트를 만든다.
$ yarn create react-app todo-app $ yarn add node-sass classnames react-icons
소스포멧팅을 쉽게 하기 위해 프리티어의 설정파일을 프로젝트 최상단에 추가한다. intellij에서 prettier를 사용하려면 플러그인에서 Prettier를 설치하면 된다. 자세한 사항은 여기를 참조한다.
// 프로젝트 최상단에 .prettierrc 설정 { "singleQuote": true, "semi": true, "useTabs": false, "tabWidth": 2, "trailingComma": "all", "printWidth": 80 }
프로젝트 최상위 디렉토리에 jsconfig.json 파일을 만들어 넣으면, import 시 해당 연관 컴포넌트가 에디터에 열려있지 않아도 자동 완성기능이 작동한다.
// jsconfig.json { "compilerOptions": { "target": "es6" } }
index.css 파일을 아래와 같이 수정한다.
// index.css 수정 body { margin: 0; padding: 0; background: #e9ecef; }
src 하위에 components 디렉토리를 만들고, 아래의 컴포넌트와 scss를 추가한다. 별도의 디렉토리를 만드는 이유는 구조상의 이유가 아니라, 관습 때문이다.
- TodoTemplate.js : 화면을 가운데 정렬, 앱 타이틀을 보여줌
- TodoInsert.js : 새로운 항목을 추가해주는 컴포넌트
- TodoListItem.js : 각 할 일을 보여주는 컴포넌트
- TodoList.js : 할 일의 목록을 보여주는 컴포넌트
// 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;
// TodoTemplate.scss .TodoTemplate { width: 512px; // width가 주어진 상태에서 좌우 중앙 정렬 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; } }
// TodoListItem.js import React from 'react'; import { MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline, } from 'react-icons/md'; import './TodoListItem.scss'; import cn from 'classnames'; const TodoListItem = ({ onToggle, todo, onRemove }) => { 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;
// 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; } }
// 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;
// TodoList.scss .TodoList { min-height: 320px; max-height: 513px; overflow-y: auto; }
// TodoInsert.js import React, { useCallback, useState } 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(''); 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;
// 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; } } }
// App.js import React, { useCallback, useRef, 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, }, ]); const nextId = useRef(4); //console.log(nextId); const onInsert = useCallback( text => { const todo = { id: nextId.current, text, checked: false, }; setTodos(todos.concat(todo)); nextId.current += 1; // nextId 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;
최신 댓글