우리는 이전의 단원들을 통해 리액트의 기본기부터 컴포넌트를 스타일링하는 방법까지를 배웠습니다. 지금까지 배운 내용을 활용하여 프런트 엔드를 공부할 때 자주 구현하는 일정관리 애플리케이션을 만들어보겠습니다.
이번 실습은 다음과 같은 순서로 진행됩니다.
일정 관리 애플리케이션을 만들기위해 터미널에 create-react-app을 사용한 명령어를 입력하겠습니다. 또한 프로젝트가 생성되면 todo-app 디렉터리로 이동하여 애플리케이션을 만들 때 필요한 라이브러리를 설치하세요.
$ yarn create react-app todo-app
$ cd todo-app
$ yarn add node-sass classnames react-icons
Prettier을 설정하여 코드 스타일링을 깔끔하게 정리합시다. .prettierrc 파일을 다음과 같이 만드세요.
// .prettierrc
{
“singleQuote“: true,
“semi“: true,
“useTabs“: false,
“tabWidth“: 2,
“trailingComma“: “all“,
“printWidth“: 80
}
글로벌 스타일 파일이 들어있는 index.css를 수정하겠습니다.
// index.css
body {
margin: 0;
padding: 0;
background: #e9ecef;
}
컴포넌트를 초기화 한 뒤, 프로젝트 디렉터리에서 yarn start 명령어를 입력하여 개발 서버를 구동하세요.
// App.js
import React from 'react';
const App = () => {
return <div>Todo App을 만들자!</div>;
};
export default App;
앞으로 만들 컴포넌트들은 다음과 같습니다. 이 컴포넌트들은 관습적으로 src 디렉터리에 components 디렉터리를 만들어 그 안에 저장합니다.
// 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;
import React from 'react';
import TodoTemplate from './components/TodoTemplate';
( ... )
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;
}
}
// 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 React from 'react';
import TodoTemplate from './components/TodoTemplate';
import TodoInsert from './components/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;
}
}
}
일정 관리 항목을 보일 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;
// 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;
두 컴포넌트의 스타일링은 다음과 같습니다.
// 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.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;
// 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;
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.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;
state가 잘 업데이트 되었는지를 확인하는 다른 방법은 리액트 개발자 도구를 사용하는 방법이 있습니다.
https://chrome.google.com/webstore/category/extensions
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;
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;
리액트 컴포넌트에서는 배열의 불변성을 지키면서 배열 원소를 지워야하는 경우, 배열의 내장함수 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]
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;
// 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.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;
수정 기능은 삭제 기능과 비슷합니다.
onToggle이라는 함수를 App에 만들고 해당 함수를 TodoList 컴포넌트에 props로 넣어줍니다. 이 함수는 TodoList를 통해 TodoListItem에게 전달합니다.
// 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;
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;
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>
);
[리액트스타터2] 12장. immer를 사용하여 더 쉽게 불변성 유지하기 (0) | 2022.12.29 |
---|---|
[리액트스타터2] 11장. 컴포넌트 성능 최적화 (0) | 2022.12.29 |
[리액트스타터2] 9장. 컴포넌트 스타일링 (0) | 2022.12.01 |
[리액트스타터2] 8장. Hooks (0) | 2022.11.24 |
[리액트스타터2] 7장. 컴포넌트의 라이프사이클 메서드 (0) | 2022.11.17 |