Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
127 changes: 126 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,130 @@
import { useEffect, useState } from 'react';
import { Global, css } from '@emotion/react';
import styled from '@emotion/styled';
import InputForm from './components/InputForm.jsx';
import TodoList from './components/TodoList.jsx';
import Header from './components/Header.jsx';
import bgImage from './grid.jpg';

const GlobalStyles = () => (
<Global
styles={css`
Comment on lines +9 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 Global 컴포넌트로 전역 스타일 설정하신 점 아주 좋습니다!!
이 부분을 별도의 컴포넌트로 분리하고 import해서 사용하면 더욱 깔끔할 것 같아요 👍🏻

@font-face {
font-family: 'RixXladywatermelonR';
src: url('https://fastly.jsdelivr.net/gh/projectnoonnu/2408-4@1.0/RixXladywatermelonR.woff2')
format('woff2');
font-weight: normal;
font-style: normal;
}
html {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
body {
font-family: 'RixXladywatermelonR', sans-serif;
max-width: 500px;
padding: 10px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
scrollbar-width: thin;
margin: 0;
}
button {
font-family: 'RixXladywatermelonR';
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap-reverse;
}
`}
/>
);
Comment on lines +9 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global style을 통해서 하나의 디자인 컴포넌트에 한번에 css를 정리할 수 있군요!🤩

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주현님에게 전역 스타일에 대해 리뷰를 들었는데 이렇게 사용하는 거군요! 확실히 훨씬 깔끔한 것 같아요 다음 미션 때 참고해서 적용해보겠습니다😆


const ContainerStyle = styled.div`
position: relative;
display: flex;
width: 450px;
height: 680px;
flex-direction: column;
background-color: #fef79fbd;
overflow: hidden;
::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(${bgImage});
background-size: cover;
background-blend-mode: darken;
opacity: 0.5;
z-index: -1;
}
`;

function App() {
return <div>React Todo</div>;
const userName = '옹헤';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹헤 이름이 너무 귀여워요...😻

Comment on lines 74 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수화 하여 사용하신 점 아주 좋습니다!!
네이밍 컨벤션 측면이긴 하지만 상수값을 사용할 때에는 UPPER_SNAKE_CASE를 사용한답니다! (참고)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또한 컴포넌트는 함수이기 때문에 상태가 업데이트 될 때마다 컴포넌트 내부에 선언된 변수는 매번 메모리에 재할당 됩니다!
하지만 상수는 재할당 될 필요가 없기 때문에 주로 컴포넌트 밖에서 선언해 줍니다 🙂

Suggested change
function App() {
return <div>React Todo</div>;
const userName = '옹헤';
const USER_NAME = '옹헤';
function App() {
// 생략
}

참고자료

//localStorage에서 todoList 불러오기
const [todoList, setTodoList] = useState(() => {
const prevTodoList = localStorage.getItem('todoList');
return prevTodoList ? JSON.parse(prevTodoList) : [];
});

//todoList가 변경될 때마다 localStorage에 저장
useEffect(() => {
localStorage.setItem('todoList', JSON.stringify(todoList));
}, [todoList]);
Comment on lines +81 to +85

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useEffect 사용해서 재랜더링 조건 부여하신거 좋은 것 같습니다!


//사용자에게 받은 입력으로 todo 추가하기
function handleAddTodo(todoInput) {
const newTodo = {
createTime: Date.now(),
Comment on lines +89 to +90
Copy link

@jaee55555 jaee55555 Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙋질문 있습니다~!
혹시 id(key)의 변수명을 createTime으로 명명하신 이유가 있을 까요?
지금 위치에서 변수명의 뜻을 바로 이해할 수 있지만,
해당 props를 받아서 사용하는 다른 컴포넌트에서는 id값이라고 명확하게 이해하기가 쉽지 않을 것 같아서요..!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 같은 이유로 궁금합니다..!

content: todoInput,
isCompleted: false,
};
setTodoList([...todoList, newTodo]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프레드 연산자를 활용해서 추가하는 방법이 훨 간단하고 보기에도 편하네요👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spread 연산자 사용 아주 좋습니다! 다만 setState를 사용할 때 변경되는 state를 직접 참조하여 사용하는 것보다는 콜백 함수를 작성하는 방식이 조금 더 안정적인 방식입니다 🙂

Suggested change
setTodoList([...todoList, newTodo]);
setTodoList((prev) => [...prev, newTodo]);

그 이유가 궁금하다면...

프로젝트를 진행하다 보면 state 변경 -> 렌더링 -> state 변경 -> 렌더링 -> ... 과정을 반복하게 됩니다.

setState(state + 1)은 현재의 state 값을 기반으로 상태를 업데이트 합니다. 하지만 이 방식은 state 값이 변경되는 과정에서 다른 setState 호출이 있을 경우, 이전 렌더링 시점의 상태를 기반으로 하기 때문에 최신 상태값을 반영하지 못할 수 있습니다.

반면 setState(prev => prev + 1)은 콜백 함수를 사용하여 이전 상태(prev)를 기준으로 상태를 업데이트 합니다. 이 방법은 상태 업데이트가 비동기적으로 처리되는 동안에도 항상 최신 상태를 반영하므로, 상태가 여러 번 업데이트 될 때에도 정확한 결과를 보장한다고 합니다!

}

function handleComplete(todo) {
const newTodoList = todoList.map((item) => {
if (item.createTime === todo.createTime) {
return { ...item, isCompleted: !item.isCompleted };
}
return item;
});
setTodoList(newTodoList);
}

function handleDelete(todo) {
const newTodoList = todoList.filter((item) => {
return item.createTime !== todo.createTime;
});
Comment on lines +108 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수가 직접 표현식을 반환하는 경우에는 중괄호와 return 키워드를 생략하여 사용할 수 있습니다!

Suggested change
const newTodoList = todoList.filter((item) => {
return item.createTime !== todo.createTime;
});
const newTodoList = todoList.filter(
item => item.createTime !== todo.createTime
);

setTodoList(newTodoList);
}

return (
<>
<GlobalStyles />
<ContainerStyle>
<Header name={userName} /> {/* Header 컴포넌트 */}
<InputForm handleAddTodo={handleAddTodo} /> {/* InputForm 컴포넌트 */}
<TodoList
todoList={todoList}
handleComplete={handleComplete}
handleDelete={handleDelete}
/> {/* TodoList 컴포넌트 */}
</ContainerStyle>
</>
);
}

export default App;
14 changes: 14 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import styled from "@emotion/styled";

const HeaderStyle = styled.header`
font-size: 40px;
font-weight: bold;
padding: 20px;
background-color: #2b3681;
color: white;
box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.5);
`;

export default function Header({ name }) {
return <HeaderStyle>{name}'s React Todo</HeaderStyle>;
}
79 changes: 79 additions & 0 deletions src/components/InputForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import styled from '@emotion/styled';
import { useState } from 'react';

const InputFormStyle = styled.form`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InputForm 컴포넌트가 메인이다 보니 스타일 관련 코드는 InputForm 아래에 위치시키는 게 좋을 것 같아요!

display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
`;

const InputLabelStyle = styled.label`
align-items: center;
padding: 2px 7px;
font-size: 20px;
`;

const InputStyle = styled.input`
border-color: black;
border-width: 0 0 2px;
background-color: transparent;
width: 180px;
margin: 0;
font-family: RixXladywatermelonR;
padding-bottom: 5px;
color: rgb(50, 50, 50);
:focus {
outline: none;
box-shadow: none;
}
`;

const InputBtnStyle = styled.button`
border: transparent;
border-radius: 4px;
background-color: transparent;
padding: 2px 7px;
font-size: 25px;
font-weight: 500;
:hover {
color: green;
background-color: transparent;
}
`;

export default function InputForm({ handleAddTodo }) {
const [todoInput, setTodoInput] = useState('');

const handleInputChange = (e) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네이밍 아주 좋습니다 👍🏻

setTodoInput(e.target.value);
};

const handleInputSubmit = (e) => {
e.preventDefault();
//입력값이 없으면 alert 띄우기
if (todoInput === '') {
alert('한 글자 이상 입력해주세요.');
return;
}
Comment on lines +54 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 문자열 방지하는 alert 좋은 것같아요~!👍

handleAddTodo(todoInput);
setTodoInput(''); // input 초기화
};

return (
<>
<InputFormStyle onSubmit={handleInputSubmit}>
<InputLabelStyle htmlFor="todo-input">할 일 : </InputLabelStyle>
<InputStyle
id="todo-input"
placeholder="할 일을 입력하세요"
value={todoInput}
onChange={handleInputChange}
/>
<InputBtnStyle id="input-btn" type="submit">
+
</InputBtnStyle>
</InputFormStyle>
</>
);
}
58 changes: 58 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import styled from '@emotion/styled';

const ListLiStyle = styled.li`
display: flex;
align-items: center;
padding: 8px 30px;
gap: 8px;
`;

const DoBtnStyle = styled.button`
width: 25px;
height: 25px;
border-radius: 8px;
background-color: transparent;
font-size: 25px;
border-color: #393939;
`;

const DoneBtnStyle = styled.button`
width: 25px;
height: 25px;
border-radius: 8px;
border: transparent;
background-color: green;
font-size: 20px;
font-weight: 500;
color: white;
`;

const DeleteBtnStyle = styled.button`
border: transparent;
background-color: transparent;
font-size: 20px;
font-weight: 500;
`;

const DoneSpanStyle = styled.span`
text-decoration: line-through;
color: #b3b3b3;
`;

export default function TodoItem({ todo, handleComplete, handleDelete }) {
return (
<ListLiStyle>
{todo.isCompleted ? (
<DoneBtnStyle onClick={() => handleComplete(todo)}>v</DoneBtnStyle>
) : (
<DoBtnStyle onClick={() => handleComplete(todo)}></DoBtnStyle>
)}
{todo.isCompleted ? (
<DoneSpanStyle>{todo.content}</DoneSpanStyle>
) : (
<span>{todo.content}</span>
)}
Comment on lines +45 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo.isCompleted 값에 따라 렌더링 하는 부분을 2개로 나누어 작성하신 이유가 있으신가요?!
아래처럼 작성하시면 더욱 깔끔할 것 같아요! 👍🏻

Suggested change
{todo.isCompleted ? (
<DoneBtnStyle onClick={() => handleComplete(todo)}>v</DoneBtnStyle>
) : (
<DoBtnStyle onClick={() => handleComplete(todo)}></DoBtnStyle>
)}
{todo.isCompleted ? (
<DoneSpanStyle>{todo.content}</DoneSpanStyle>
) : (
<span>{todo.content}</span>
)}
{todo.isCompleted ? (
<>
<DoneBtnStyle onClick={() => handleComplete(todo)}>v</DoneBtnStyle>{' '}
<DoneSpanStyle>{todo.content}</DoneSpanStyle>
</>
) : (
<>
<DoBtnStyle onClick={() => handleComplete(todo)}></DoBtnStyle>
<span>{todo.content}</span>
</>
)}

<DeleteBtnStyle onClick={() => handleDelete(todo)}>x</DeleteBtnStyle>
</ListLiStyle>
Comment on lines +44 to +56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 삼항연산자로 조건부 렌더링하니까 정말 간편하네요.. 이야 깔끔하다~ !

);
}
61 changes: 61 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import styled from '@emotion/styled';
import TodoItem from './TodoItem';

const SectionStyle = styled.section`
margin-top: 20px;
`;

const SectionTitleStyle = styled.h4`
font-size: 20px;
text-align: left;
padding: 10px 20px;
margin: 0;
`;

const ListUlStyle = styled.ul`
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
height: 168px;
overflow-y: scroll;
`;

export default function TodoList({ todoList, handleComplete, handleDelete }) {
// todoList에서 완료되지 않은 todoList와 완료된 todoList를 분리
const doList = todoList.filter((todo) => !todo.isCompleted);
const doneList = todoList.filter((todo) => todo.isCompleted);

Comment on lines +26 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter 사용해서 doList와 doneList에 나눠서 처리해주시는거 좋은 것같아요!🤩

return (
<>
<SectionStyle>
<SectionTitleStyle>📋 Todo ({doList.length})</SectionTitleStyle>
<ListUlStyle>
{doList.map((todo) =>
<TodoItem
key={todo.createTime}
todo={todo}
handleComplete={handleComplete}
handleDelete={handleDelete}
/>
)}
</ListUlStyle>
</SectionStyle>
<SectionStyle>
Comment on lines +34 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분을 컴포넌트로 한 번 더 분리해도 좋을 것 같아요!

<SectionTitleStyle>👍🏻 Done ({doneList.length})</SectionTitleStyle>
<ListUlStyle>
{doneList.map((todo) =>
<TodoItem
key={todo.createTime}
todo={todo}
handleComplete={handleComplete}
handleDelete={handleDelete}
/>
)}
</ListUlStyle>
</SectionStyle>
</>
);
}
Binary file added src/grid.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading