과제 : 리액트 할일 목록 만들기 프로젝트 (todo list)
- state, CRUD 활용
- localstorage 활용
- custom 스타일링
- netlify에 재배포하기
투두리스트 기능 = 할일 생성(추가)하기, 완료한 할일은 완료표시 하기(다시 누르면 취소) , 삭제하기
사용자가 폼을 작성하고 "추가" 버튼을 누르면 새로운 할일이 생성됩니다.
먼저 개발할 프로젝트에 npx create-react-app [todo-app(앱이름)] 으로 생성한후
index.js에서는 <React.StrictMode>를 state 렌더링 중복 방지하기 위해 제거한다. 이 컴포넌트를 쓰는 이유는 리액트 자체에서 개발 환경에서 잠재적인 문제를 감지하고 해결할 수 있도록 도와주는 도구라고 간단히 알고 있으면 된다.
메인 App.js
일단 App.css 링크를 연결(import)한 후
할일 객체를 관리하기위해 useState를 이용한다.
App.js
import React, { useState } from "react";
import { Container, Form, TextInput, SubmitInput,
randomImageUrls, Container, Form, TextInput,
SubmitInput, UnorderdList, ListItem, TodoText,
TodoDelete, randomImageUrls } from "./styledComponents";
function App() {
const [todo, setTodo] = useState([]); //todo 배열
const [todoId, setTodoId] = useState(0); //각 todo의 ID
// 각 todo의 배경이미지 소스 랜덤하게 추출
const getRandomImageUrl = () => {
const randomIndex = Math.floor(Math.random() * randomImageUrls.length);
console.log(randomImageUrls[randomIndex]);
return randomImageUrls[randomIndex];
};
/*
할일이 단순히 문자열이면 안 되는 이유!
- 삭제나 수정을 할 때 구분할 방법이 없다.
=> 따라서 하나의 할일은 하나의 객체로 관리하는 것이 좋다.
*/
//새로운 할일 객체를 생성하고 현재 할일 목록에 추가
const handleSubmit = (todoText) => {
setTodo([
...todo,
{
todoText: todoText, //텍스트
todoId: todoId, //아이디
todoDone: false, // 완료여부
backgroundImage: getRandomImageUrl(), //배경이미지 소스
},
]);
setTodoId(todoId + 1);
console.log(todo.backgroundImage);
};
return (
<Container>
<Form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(e.target.todo.value);
e.target.todo.value = ""; //추가버튼을 누르면 input clear
}}>
<TextInput type="text" placeholder="할일 쓰기" name="todo" />
<SubmitInput type="submit" value="추가" />
</Form>
<UnorderdList>
{todo.map((item, index) => {
return (
<ListItem
key={index}
onClick={() => {
handleToggle(item.todoId); // todo 완료 여부 토글
}}
backgroundImage={item.backgroundImage} // styled-components에 참조할
스타일 속성
todoDone={item.todoDone ? "block" : "none"}> // styled-components에 참조할
완료 이미지 추가여부 (활성화/비활성화)
<TodoText className="todoText">{item.todoText}</TodoText>
//todo삭제
<TodoDelete
onClick={(e) => {
e.stopPropagation(); //stop event propagation
handleDelete(item.todoId); // delte 함수
}}></TodoDelete>
</ListItem>
);
})}
</UnorderdList>
</Container>
);
}
//App.css
*{
box-sizing: border-box;
}
html, body, #root{
height: 100%;
}
html{
background-color: #BACAEB;
}
★(custom) 폰트 설정
@font-face {
font-family: 'Cafe24Supermagic-Bold-v1.0';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2307-2@1.0/Cafe24Supermagic-Bold-v1.0.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
★(custom) todo폰트에 적용
.todoText {
font-family: 'Cafe24Supermagic-Bold-v1.0';
font-weight: 700;
}
또한 효율적인 스타일링과 깔끔하게 코드작성을 위해 styled-components 라이브러리를 사용하여 npm install styled-components
커스텀 컴포넌트로 구성하였다.
//styledComponents.js
import styled from "styled-components";
//이미지 src
export const randomImageUrls = ["/img/post-it1.png", "/img/post-it2.png",
"/img/post-it3.png", "/img/post-it4.png", "/img/post-it5.png"];
export const Container = styled.div`
width: 100%;
height: 100%;
padding: 1.5rem 0 0;
display: flex;
flex-direction: column;
align-items: center;
`;
export const Form = styled.form`
width: 33%;
min-width: 375px;
display: flex;
`;
export const TextInput = styled.input`
width: 85%;
height: 3rem;
padding: 0.5rem;
border: none;
border-radius: 0.2rem;
margin-right: 15px;
font-size: 1.2rem;
&:focus {
outline: none;
}
`;
export const SubmitInput = styled.input`
width: 25%;
height: 3rem;
border: none;
border-radius: 0.2rem;
color: #ffffff;
background-color: #5a75aa;
font-size: 1.2rem;
`;
export const UnorderdList = styled.ul`
display: flex;
flex-wrap: wrap; /* ★줄바꿈 설정 */
width: 73%;
min-width: 375px;
padding: 0;
list-style-type: none;
★(custom) 한줄당 3개의 리스트를 보여주고 그 이상은 다음 줄에 배치
> * {
width: 30%;
}
`;
export const ListItem = styled.li`
height: 250px;
padding: 1.575rem;
margin: 0.5rem;
★(custom) 추가할때마다 할일 객체의 배경이미지가 랜덤으로 설정하여 반영
props참조 접근방식으로 backgroundImage 불러온 값으로 설정
background: ${(props) => (props.backgroundImage ? `url(${props.backgroundImage}) center/contain no-repeat` : "none")};
background-size: 80% 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
`;
export const TodoText = styled.span`
display: inline-block;
width: 90%;
font-size: 1.2rem;
line-height: 1.5rem;
text-align: center;
line-height: 30px;
`;
사용자가 할일 항목을 클릭하면 해당 할일의 완료 상태가 토글됩니다.
App.js
const handleToggle = (todoId) => {
setTodo(
todo.map((item, index) => {
return item.todoId === todoId ? { ...item, todoDone: !item.todoDone } : item;
})
);
};
return (
<ListItem
key={index}
onClick={() => {
handleToggle(item.todoId);
}}
backgroundImage={item.backgroundImage}
// ★(custom) ListItem을 누를 시 todoDone의 상태값에 따라 block or none
스타일 속성이 스타일링 컴포넌트에 에 전달
todoDone={item.todoDone ? "block" : "none"}>
<TodoText className="todoText">{item.todoText}</TodoText>
</ListItem>
)
styledComponents.js
export const ListItem = styled.li`
height: 250px;
padding: 1.575rem;
margin: 0.5rem;
background: ${(props) => (props.backgroundImage ? `url(${props.backgroundImage}) center/contain no-repeat` : "none")};
background-size: 80% 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
★(custom) todoDone props에 따라 block(보여주기)|| none(안보여주기) display설정
이미지 아이콘으로 decoration
&:before {
content: "";
display: ${(props) => props.todoDone};
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
background: url("/img/success.png") no-repeat center/cover;
}
`;
사용자가 할일 항목의 삭제 버튼을 클릭하면 해당 할일이 삭제됩니다.
App.js
const handleDelete = (todoId) => {
setTodo(todo.filter((item) => item.todoId !== todoId));
};
.
.
.
.
return(
.
.
.
<UnorderdList>
{todo.map((item, index) => {
return (
<ListItem
key={index}
onClick={() => {
handleToggle(item.todoId);
}}
backgroundImage={item.backgroundImage}
todoDone={item.todoDone ? "block" : "none"}>
<TodoText className="todoText">{item.todoText}</TodoText>
//todo 삭제
<TodoDelete
onClick={(e) => {
e.stopPropagation(); //stop event propagation
handleDelete(item.todoId);
}}></TodoDelete>
</ListItem>
);
})}
</UnorderdList>
)
styledComponents.js
export const TodoDelete = styled.button`
width: 20px;
height: 20px;
border: none;
border-radius: 0.5rem;
right: 0;
top: -1%;
★(custom) 쓰레기통 아이콘으로 디자인변경
background: url("/img/trash-can.png") no-repeat center/cover;
z-index: 100;
position: absolute;
`;
-로컬 스토리지는 클라이언트 측 웹 브라우저에서 데이터를 키-값 쌍으로 저장하는 웹 스토리지 메커니즘입니다.
<활용>
localStorage.getItem(key)
: 지정한 키(key)에 해당하는 값을 가져옵니다.
localStorage.setItem(key, value)
: 지정한 키(key)에 값을 저장합니다.
localStorage.removeItem(key)
: 지정한 키(key)의 값을 제거합니다.
JSON.stringify()와 JSON.parse()
: 로컬 스토리지에는 문자열만 저장할 수 있으므로, 객체를 문자열로 변환하여 저장하고 다시 객체로 변환하여 사용한다.
App.js
import React, { useState, useEffect } from "react";
useEffect(() => {
const defaultTodo = JSON.parse(localStorage.getItem("todo"));
if (!defaultTodo) return;
setTodo(defaultTodo);
if (defaultTodo.length !== 0) {
setTodoId(defaultTodo[defaultTodo.length - 1].todoId + 1);
}
}, []);
useEffect(() => {
localStorage.setItem("todo", JSON.stringify(todo));
}, [todo]);
첫번째 useEffect는 컴포넌트가 마운트될 때 한 번 실행된다.
로컬 스토리지에서 "todo" 키로 저장된 데이터를 가져와서 defaultTodo에 할당한다.
defaultTodo가 존재하면, 해당 데이터를 todo 상태로 설정하고, 마지막 할 일 ID를 계산하여 todoId 상태로 설정한다.
두번째 useEffect는 : todo 상태가 변경될 때마다(추가,삭제,수정) 실행된다.
todo 상태를 로컬 스토리지에 "todo" 키로 문자열로 변환(JSON.stringify)하여 저장합니다.
완성!!
할일을 추가하면 포스트잇이 랜덤으로 붙여지고 휴지통 아이콘으로 삭제하기, 완료 포스트잇을 누르면 100% 아이콘이 꽝 찍힌다~!
이걸 나만 이용하면 아까우니(?) 웹사이트로 배포해보자!!
netlify 배포하는 법은 저번 포스팅때 했으니 다시 빌드하는 걸 재배포 하는 방법과 사이트 주소 변경하는 법을 알아보자..!
npm run build
꼭 처음에 하는거 잊지 않으셨죠? 업뎃할때도 이전 bulid한걸 삭제하고 다시 빌드하면 된다.
사이트 들어가서 로그인한 다음 Site 목록에서 Deploys 탭을 클릭 !!
저기 동그라미 친 부분에 build 파일을 드래그앤드롭 하면 바로 다시 반영된다!
사이트 이름 바꾸는 부분은 이 형광부분으로 들어가면 바꿀수 있음
배포 링크 ▼
https://nipa-frontend-5-todo-jaeum.netlify.app/