리액트를 사용해서 TodoList를 만드는 소규모 프로젝트를 진행해보겠다. 패스트캠퍼스의 강의를 참조하였습니다!
리액트 프로젝트를 하나 만든 후에 2개의 라이브러리를 설치할 것이다.
yarn add styled-components react-icons
여러 컴포넌트를 만들고 관리할 것인데 간단한 역할을 먼저 알아보자.
TodoTemplate
Todolist의 레이아웃을 담당하고 하얀색 테두리를 담당한다.
TodoHead
날짜시간, 할일의 개수를 가진다
TodoList
할일에 대한 정보를 map내장함수를 사용하여 랜더링할것이다.
TodoItem
할일을 표시한다.
TodoCreate
새로운 할일을 등록할 수 있게 한다.
1. 회색배경화면 만들기
//App.js
import React from 'react'
import {createGlobalStyle} from 'styled-components';
const GlobalStyle = createGlobalStyle`
body {
background : #e9ecef;
}
`;
function App() {
return (
<>
<GlobalStyle />
<div>hi</div>
</>
);
}
export default App;
먼저 회색 배경화면을 GlobalStyle을 이용해서 만들어주었다.
2. 하얀색 박스 만들기 (TodoTemplate)
//TodoTemplate.js
import React from 'react';
import styled from 'styled-components';
const TodoTemplateBlock = styled.div`
width : 512px;
height : 768px;
position : relative;
background : white;
border-radius : 16px;
box-shadow : 0 0 8px rgba(0,0,0,0.04);
margin : 0 auto;
margin-top : 96px;
margin-bottom : 32px;
display : flex;
flex-direction : column;
`;
function TodoTemplate({children}) {
return (
<TodoTemplateBlock>{children}</TodoTemplateBlock>
);
}
export default TodoTemplate;
components파일 안에 TodoTemplate를 만들어서 App.js와 연동시켜주었다.
3. TodoHead
//TodoHead.js
import React from 'react';
import styled from 'styled-components';
const TodoHeadBlock = styled.div`
padding-top : 48px;
padding-left : 32px;
padding-right : 32px;
padding-bottom : 24px;
border-bottom : 1px solid #e9ecef;
h1 {
margin : 0;
font-size : 36px;
color : #343a40;
}
.day {
margin-top : 4px;
color : #868e96;
font-size : 21px;
}
.tasks-left {
color : #20c997;
font-size : 18px;
margin-top :40px;
font-weight: bold;
}
`;
function TodoHead() {
return (
<TodoHeadBlock>
<h1>2021년 12월 27일</h1>
<div className='day'>월요일</div>
<div className='task-left'>할일 2개 남음</div>
</TodoHeadBlock>
);
}
export default TodoHead;
마찬가지로 위와같이 설정을 해두면 된다. 우선 글자로 처리해주었고 기능을 추가할때 변수들로 추가하여 동작하게 만들어 보겠다.
function App() {
return (
<>
<GlobalStyle />
<TodoTemplate>
<TodoHead />
</TodoTemplate>
</>
);
}
4. TodoList
List가 나올 공간을 확인해보자.
//TodoList.js
import React from 'react';
import styled from 'styled-components';
const TodoListBlock = styled.div`
flex : 1; /* 자신이 차지할 수 있는 모든 영역을 차지 */
padding : 20px 32px;
padding-bottom : 48px;
overflow-y : auto;
background : gray;
`;
function TodoList() {
return (
<TodoListBlock>TodoList</TodoListBlock>
);
}
export default TodoList;
회색으로 공간처리가 잘된것을 볼 수 있다. 이제 다시
background : auto
로 바꾸어서 회색이 안보이게 설정하면 되겠다.
5. TodoItem
이제 내용물을 작성해보자.
//TodoItem.js
import React from 'react';
import styled,{css} from 'styled-components';
import { MdDone, MdDelete } from 'react-icons/md' //react-icon에서 가져옴
const Remove = styled.div`
opacity : 0;
display : flex;
align-items : center;
justify-content : center;
color : #dee2e6;
font-size : 24px;
cursor : pointer;
&:hover {
color : #ff6b6b;
}
`; //쓰레기통 아이콘 표시
const CheckCircle = styled.div`
width : 32px;
height : 32px;
border-radius : 16px;
border : 1px solid #ced4da;
font-size : 24px;
display : flex;
align-items: center;
justify-content : center;
margin-right : 20px;
cursor : pointer;
${props => props.done && css`
border : 1px solid #38d9a9;
color : #38d9a9;
`}
`; //체크 아이콘 표시
const Text = styled.div `
flex : 1;
font-size : 21px;
color : #495057;
${props => props.done && css`
color : #ced4da;
`}
`; //Text표시
const TodoItemBlock = styled.div`
display : flex;
align-items : center;
padding-top : 12px;
padding-bottom : 12px;
&:hover {
${Remove} { /* remove컴포넌트에서 만든 classname*/
opacity : 1; /* 커서를 올렸을때 1 */
}
}
`; //위 3개의 내용을 포함
function TodoItem( {id,done,text}) {
return (
<TodoItemBlock>
<CheckCircle done={done} >{done && <MdDone/>}</CheckCircle>
<Text done={done}>{text}</Text>
<Remove>
<MdDelete/>
</Remove>
</TodoItemBlock>
);
}
export default TodoItem;
Item은 조금더 복잡한데, 총 4개의 컴포넌트를 스타일링해주었고 살펴볼 것으로는 TodoitemBlock에서 ${Remove}를 가져와서 사용함으로써 커서를 올렸을때 쓰레기통이 보여지게끔 만들수 있다는 것이다.
icon은 React-icons 모듈에서 가져왔다.
https://supersfel.tistory.com/131?category=1063079
이전에 적은 17번째 글에서 내용을확인할 수 있다.
6.TodoCreate
//TodoCreate.js
import React , {useState} from 'react';
import styled, {css} from 'styled-components';
import {MdAdd} from 'react-icons/md';
const CircleButton = styled.button `
background : #38d9a9;
&:hover {
background : #63e6be;
}
&:active {
background : #20c997;
}
z-index : 5; /*내용을 가리기 위함*/
cursor : pointer;
width : 80px;
height : 80px;
display : flex;
align-items : center;
justify-content : center;
position : absolute;
left : 50%;
bottom : 0px;
transform : translate(-50%,50%); /* 정확한 버튼위치 찾기를 위함*/
font-size : 60px;
color : white;
border-radius : 40px;
border : none;
outline : none;
transition : 0.125s all ease-in;
${props => props.open && css` /* 눌렀을때 x로 보이게끔 */
background : #ff6b6b;
&:hover {
background : #ff8787;
}
&:active {
background : #fa5252;
}
transform : translate(-50%,50%) rotate(45deg);
`}
`;
const InsertFormPositioner = styled.div`
width : 100%;
bottom : 0;
left : 0;
position : absolute;
`;
const InsertForm = styled.div`
background : #f8f9fa;
padding : 32px;
padding-bottom : 72px;
border-bottom-left-radius : 16px; /* 둥근 모서리가 삐져나오지 않게*/
border-bottom-right-radius : 16px;
border-top : 1px solid #e9ecef;
`;
const Input = styled.input`
padding : 12px;
border-radius : 4px;
border : 1px solid #dee2e6;
width : 100%;
outline : none;
font-size : 18px;
box-sizing : border-box;
`;
function TodoCreate() {
const [open,setOpen] = useState(false);
const onToggle = () => setOpen(!open);
return (
<>
{open && (
< InsertFormPositioner >
< InsertForm >
<Input placeholder='할 일을 입력후, Enter를 누르세요' autoFocus />
</InsertForm>
</InsertFormPositioner >
)}
<CircleButton onClick ={onToggle} open={open}>
< MdAdd />
</CircleButton>
</>
);
}
export default TodoCreate;
조금 길어서 접은글 안에 넣어두었다.
실제 체크 박스를 가져오고, 눌렀을때 MdAdd 버튼을 돌리고 색을 변경한다.
코드가 길어서 헷갈릴 순 있으나 대부분 시각효과 설정이고 useState를 통해 open값으로 열렸을때와 닫혔을때의 값을 관리하고, 열렸을때 Input태그가 있는 Form을 열어준다는게 핵심 기능이다.
Context API활용
Context API를 활용하지 않고 Props만 가지고 컴포넌트를 넘겨준다면
위와 같은 상태가 된다. 모든 정보가 App에 정보가 있는 것이다.
Context API를 활용하게되면
위처럼 따로 Props를 넘겨주지 않아도 괜찮다.
상태 업데이트만 관리해주면 되게 되는 것이다.
그렇다고 Context API를 무조건 쓰는게 맞지는 않다. 프로젝트의 규모가 엄청 크지않으면 굳이 필요없지만 규모가클 수록 필요하다.
7. reducer 만들기
상태 관리를 위해 Reducer을 사용하여 만들 것이다.
//TodoContext.js
import React,{useReducer, createContext, useContext, useRef} from 'react';
import TodoList from './TodoList';
const initialTodos = [
{
id : 1,
text : '민규의 첫번째 할일',
done : true,
},
{
id : 2,
text : '민규의 두번째 할일',
done : true,
},
{
id : 3,
text : '민규의 세번째 할일',
done : false,
},
{
id : 4,
text : '민규의 네번째 할일',
done : false,
},
];
function todoReducer(state,action) {
switch (action.type){
case 'CREATE' :
return state.concat(action.todo);
case 'TOGGLE' :
return state.map(
todo => todo.id === action.id ? { ...todo,done : !todo.done } : todo
);
case 'REMOVE' :
return state.filter(todo => TodoList.id !== action.id);
default :
throw new Error(`Unhandled action type : ${action.type}`);
}
}
const TodoStateContext = createContext();
const TodoDispatchContext = createContext();
const TodoNextIdContext = createContext(); //id값 관리
export function TodoProvider({children}) {
const [state,dispatch] = useReducer(todoReducer, initialTodos);
const nextId = useRef(5);
return (
<TodoStateContext.Provider value = {state}>
<TodoDispatchContext.Provider value = {dispatch}>
<TodoNextIdContext.Provider value = {nextId}>
{children}
</TodoNextIdContext.Provider>
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
export function useTodoState() { //나중에 Todolist에서 그냥 const state = useTodoState();로 선언해서 사용이 가능하도록
const context = useContext(TodoStateContext);
if (!context) {
throw new Error('Cannot find TodoProvider');
}
return context;
}
export function useTodoDispatch() { //랜더링할 필요가 없을때 불필요한 랜더링을 막기위해서 나누어서 만듬
const context = useContext(TodoDispatchContext);
if (!context) {
throw new Error('Cannot find TodoProvider');
}
return context;
}
export function useTodoNextId() {
const context = useContext(TodoNextIdContext);
if (!context) {
throw new Error('Cannot find TodoProvider');
}
return context;
}
동작방식은
https://supersfel.tistory.com/127?category=1063079
위글을 다시한번 보자
TodoList에서 state를 찍어보면
function TodoList() {
const state = useTodoState();
console.log(state)
return (
<TodoListBlock>
<TodoItem text ="프로젝트 생성하기" done = {true}></TodoItem>
<TodoItem text ="민규" done = {true}></TodoItem>
<TodoItem text ="정민규" done = {false}></TodoItem>
<TodoItem text ="나는 정민규" done = {true}></TodoItem>
</TodoListBlock>
);
}
잘 나오고 있음을 확인할 수 있다.
8. 기능 구현하기
8-1 ) Todohead
function TodoHead() {
const todos = useTodoState();
const undoneTasks = todos.filter(todo => !todo.done); //할일 개수파악
const today = new Date();
const dateString = today.toLocaleDateString('ko-KR', {
year : 'numeric',
month : 'long',
day : 'numeric'
});
const dayName = today.toLocaleDateString('ko-KR', {
weekday : 'long'
});
return (
<TodoHeadBlock>
<h1>{dateString}</h1>
<div className='day'>{dayName}</div>
<div className='task-left'>할일 {undoneTasks.length}개 남음</div>
</TodoHeadBlock>
);
}
export default TodoHead;
8-2 ) TodoList
<TodoListBlock>
<TodoItem text ="프로젝트 생성하기" done = {true}></TodoItem>
<TodoItem text ="민규" done = {false}></TodoItem>
<TodoItem text ="정민규" done = {true}></TodoItem>
<TodoItem text ="나는 정민규" done = {true}></TodoItem>
</TodoListBlock>
위 같았던 코드를 아래처럼 바꾸어주면 된다.
<TodoListBlock>
{todos.map (
todo => <TodoItem
key = {todo.id}
id={todo.id}
text={todo.text}
done = {todo.done}
/>
)}
</TodoListBlock>
8-3 TodoCreate
function TodoCreate() {
const [open,setOpen] = useState(false);
const [value,setValue] = useState(''); //input값 관리
const onToggle = () => setOpen(!open);
const onChange = (e) => setValue(e.target.value);
const onSubmit = e => {
e.preventDefault(); //입력후 엔터키를 눌러도 새로고침이 안된다
dispatch({
type : 'CREATE',
todo : {
id : nextId.current,
text : value,
done : false,
}
});
setValue('');
setOpen(false);
nextId.current +=1;
}
const dispatch = useTodoDispatch();
const nextId = useTodoNextId();
return (
<>
{open && (
< InsertFormPositioner >
< InsertForm onSubmit={onSubmit}>
<Input
placeholder='할 일을 입력후, Enter를 누르세요'
autoFocus
onChange={onChange}
value={value}
/>
</InsertForm>
</InsertFormPositioner >
)}
<CircleButton onClick ={onToggle} open={open}>
< MdAdd />
</CircleButton>
</>
);
}
그러면 입력 기능이 추가되게 된다.
'프로젝트 > 소규모프로젝트들' 카테고리의 다른 글
라즈베리파이로 웹서버 만들기 (1) - 밖에서 라파 접속하기 (3) | 2023.01.24 |
---|---|
mdx Editor만들기 (마크다운 에디터) (1) | 2022.11.27 |
MFC를 이용한 디지털신호처리 프로그램 (0) | 2021.12.15 |
JS_독서관리 시스템 (2) | 2021.12.10 |
GitHub 클론코딩-2 (0) | 2021.11.27 |