Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1주차] 송은수 미션 제출합니다. #6

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
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
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
112 changes: 73 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,92 @@
# 1주차 미션: Vanilla-Todo

# 서론
# 배포링크

안녕하세요 🙌🏻 19기 프론트엔드 운영진 **변지혜**입니다.
[todoList](https://ceos-week1-vanilla-todo.vercel.app/)

이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다.
# 기능구현
- 오늘 날짜 생성
- todo 추가, 추가 시 validation 체크 및 enter로 입력
- todo 삭제
- todo 완료갯수 추가
- todo 완료/미완료 여부에 따른 이동
- localstorage 저장

비교적 가벼운 미션인 만큼 코드를 짜는 데 있어 여러분의 **창의성**을 충분히 발휘해 보시기 바랍니다. 작동하기만 하면 되는 것보다 같은 코드를 짜는 여러가지 방식과 패턴에 대해 고민해 보시고, 본인이 생각한 가장 창의적인 방법으로 코드를 작성해 주세요. 여러분이 미션을 수행하는 과정에서 겪는 고민과 생각의 깊이만큼 스터디에서 더 많은 것을 얻어가실 수 있을 것입니다.
# 느낀점

막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 슬랙 Q&A 채널이나 프론트엔드 카톡방에 편하게 질문을 남겨 주세요!
js만을 사용해서 동작을 만드려면 태그부터 속성까지 하나하나 만들어줘야 해서 손이 많이 가는 것 같습니다.

# 미션
리액트의 state처럼 값이 변하면 리렌더링이 이루어지지 않기 때문에 값이 변할때마다 직접 DOM조작을 해줘야 했습니다.

## 미션 목표
뿐만 useEffect 등의 빌트인 함수도 없어서 리액트의 필요성을 느꼈습니다.

- VSCode, Prettier를 이용하여 개발 환경을 관리합니다.
- HTML/CSS의 기초를 이해합니다.
- JavaScript를 이용한 DOM 조작을 이해합니다.
- Vanilla Js를 이용한 어플리케이션 상태 관리 방법을 이해합니다.
# 개선할 점

## 기한
localstorage에 저장하는 과정에서 todo,done으로 구분을 하여 중복되는 코드가 생겼는데, 객체배열형태로 저장해 `isDone`변수를 추가해 완료/미완료를 구분하는 방식으로 짜면 중복되는 코드를 줄일 수 있을 거 같습니다.

- 2024년 3월 15일 금요일
# Key Questions

## Key Questions
## DOM은 무엇인가요?

- DOM은 무엇인가요?
- HTML (tag) Element를 JavaScript로 생성하는 방법은 어떤 것이 있고, 어떤 방법이 가장 적합할까요?
- Semantic tag에는 어떤 것이 있으며, 이를 사용하는 이유는 무엇일까요?
- Flexbox Layout은 무엇이며, 어떻게 사용하나요?
- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요?
- 코드에서 주석을 다는 바람직한 방법은 무엇일까요?
브라우저가 이해할 수 있는 자료구조로 프로퍼티와 메서드를 제공하는 트리 자료구조입니다.

## 필수 요건
렌더링 엔진이 HTML 문서를 파싱하면 문서, 요소, 속성, 텍스트등 모두 노드객체로 변하고 이 노드 객체들로 구성된 트리 자료구조를 DOM 이라 합니다.

- [결과 화면](https://vanilla-todo-18th-modsiw.vercel.app/)의 기능을 구현합니다. (날짜, 요일별 todo 개수)
- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다.
- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다.
- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다.
- 함수와 변수의 이름은 lowerCamelCase로 짓습니다.
- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다.
js를 사용해 DOM을 조작하여 html의 구조를 변경합니다.

## 선택 요건
## HTML (tag) Element를 JavaScript로 생성하는 방법은 어떤 것이 있고, 어떤 방법이 가장 적합할까요?

- 외부 폰트([눈누 상업용 무료폰트](https://noonnu.cc/))로 입맛에 맞게 꾸밉니다.
- 브라우저의 `localStorage` 혹은 `sessionStorage`를 이용하여 다음 번 접속 시에 기존의 투두 데이터를 불러옵니다.
- 이 외에도 추가하고 싶은 기능이 있다면 마음껏 추가하셔도 됩니다.
1. createElement

# 링크 및 참고자료
```js
const todoCard = document.createElement('div');
```

- [HTML/CSS 기초](https://heropy.blog/2019/04/24/html-css-starter/)
- [HTML 태그](https://heropy.blog/2019/05/26/html-elements/)
- [FlexBox 가이드](https://heropy.blog/2018/11/24/css-flexible-box/)
- [JS를 통한 DOM 조작](https://velog.io/@bining/javascript-DOM-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0#append)
- [localStorage, sessionStorage](https://www.daleseo.com/js-web-storage/)
- [git 사용법](https://wayhome25.github.io/git/2017/07/08/git-first-pull-request-story/)
- [좋은 코드리뷰 방법](https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/)
2. insertAdjacentHTML

```js
element.insertAdjacentHTML(position, '<div class="todoCard"></div>');
```

태그요소를 만들때는 만드는 거에만 집중하고, 위치를 정하는 것은 나중에 정하는 게 가독성이 좋아보여
createElement방식이 더 적합하다고 생각합니다.

## Semantic tag에는 어떤 것이 있으며, 이를 사용하는 이유는 무엇일까요?

시맨틱 태그는 말 그대로 의미가 담긴 태그로, header, nav, section, article, aside, footer 등이 있습니다.

코드를 읽기 쉽게 해줍니다. 내가 짠 코드는 나만 읽는 게 아니라 같은 팀원도 읽고, 시간이 지나면 나도 내가 짠 코드를 기억하지 못할 수 있기 떄문에 가독성 좋은 코드가 필요하고, 시맨틱 태그는 코드의 가독성을 높이는데 도움을 줍니다.

또한 SEO를 향상시킵니다. 검색엔진이 검색을 할 때, 브라우저가 소스코드를 읽을 때 도움을 줍니다.

## Flexbox Layout은 무엇이며, 어떻게 사용하나요?

반응형 웹사이트를 만들 때 주로 사용합니다. 뷰포트의 크기에 따라 요소들이 자리를 잡고, 뷰포트의 움직임에 맞춰 반응형으로 위치가 조정됩니다.

`display: flex`를 부모요소에 적용합니다.

`flex-direction`을 통해 방향을 결정합니다. 기본 값은 `row`이고, `row | row-reverse | column | column-reverse`를 사용할 수 있습니다.

`flex-wrap`을 통해 줄바꿈을 적용할지 말지 선택할 수 있습니다.

정렬을 위해 `justify-content`, `align-items`를 사용합니다. 각각 축방향, 축의 수직방향으로 정렬을 합니다.

`align-self`를 사용해 자식 중 하나의 요소만 위치를 조정할 수 있습니다. `justify-self`는 존재하지 않습니다.

`gap`을 사용해 자식요소들간의 거리를 조정할 수 있습니다.

`flex-grow`를 통해 남는 공간을 채울 수 있습니다. 대부분의 경우 `width/height`속성과 함께 쓰입니다.

`flex-shrink`를 통해 요소가 수축하지 않게 설정합니다.

`flex-basis`를 통해 요소의 기본 크기를 설정합니다.

## JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요?

자료형이 미리 정해져 있지 않고 런타임에 동적으로 정해지기 때문에 유연하게 타입이 정해진 다는 것이 장점이고, 웹(브라우저) 뿐만 아니라 nodejs 등 서버환경에서도 사용할 수 있습니다. 또한 다양한 라이브러리, 프레임워크가 있어 원하는 기능을 쉽게 구현할 수 있다는 것도 장점이라 생각합니다.

## 코드에서 주석을 다는 바람직한 방법은 무엇일까요?

주석을 다는 이유는 다른 사람이 알아보기 위함이고, 유지보수를 보다 쉽게 하기 위해서입니다. 따라서 해당 변수, 함수가 어떤 용도로 사용되고 있는지 이해할 수 있게 달아야 합니다.

개발환경에서도 호흡이 길어지면 내가 하고 있는 것을 적어가면서 하는게 편리하다고 생각하는데, 이런 주석들은 배포이전에 지워줘도 괜찮을 것 같습니다.
36 changes: 34 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,45 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
<title>Vanilla Todo</title>
<link rel="stylesheet" href="style.css" />
</head>

<body>
<div class="container"></div>
<main class="wrapper">
<header class="todoHeader">
<div class="headerDMY">
<span class="headerDate"></span>
<div class="headerMY">
<span class="headerMonth"></span>
<span class="headerYear"></span>
</div>
</div>
<div class="headerDay"></div>
</header>
<section class="todoSection">
<span>Todo</span>
</section>
<section class="doneSection">
<span>Done</span>
</section>
<section class="todoInputGroup">
<input
type="text"
placeholder="할 일을 입력해주세요..."
class="todoInput"
/>
<button class="addTodo">+</button>
</section>
<footer class="todoFooter">
Copy link

Choose a reason for hiding this comment

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

저는 header과 footer가 일반적인 웹사이트에서만 사용되는거라 생각했었는데, 훨씬 가독성도 좋고 이해하기 편하네요!

<span class="todoProgress"></span>
<span class="todo">todo List</span>
</footer>
</main>
</body>
<script src="script.js"></script>
</html>
190 changes: 189 additions & 1 deletion script.js
Original file line number Diff line number Diff line change
@@ -1 +1,189 @@
//CEOS 19기 프론트엔드 파이팅🔥 ദ്ദി˶ˊᵕˋ˵)
/*
checkboxNumber: todoCard가 unique한 id를 갖기 위한 변수
*/
let checkboxNumber = 0;
Copy link

Choose a reason for hiding this comment

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

각각의 카드에 대해 다른 디자인이나 요소가 들어가는게 아닌 이상 id를 부여하지 않아도 될 듯합니다! 하지만 만약 복잡하게 관리해야하는 프로젝트에서는 좋은 방법이 될거 같아요!

Copy link
Member

Choose a reason for hiding this comment

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

checkboxNumber를 createTodoCard 함수 내에서 관리하도록 변경하면 좋을 것 같아요. 해당 변수는 DOM 요소의 ID를 관리하기 위해 사용되는 것 같은데, 각 TODO 항목을 생성할 때마다 고유 ID를 생성하는 로직으로 대체하면 좋을 것 같아요.
전역변수는 코드의 길이가 길어지면 가독성을 해치고, 버그 발생의 원인이 있다고 하여 되도록이면 함수내에서 쓰는게 좋다고 합니다!


//todoCard 만들기
const createTodoCard = (todo, isDone) => {
const todoCard = document.createElement('div');
const checkbox = document.createElement('input');
const todoContent = document.createElement('label');
const todoDelete = document.createElement('div');

todoCard.className = 'todoCard';
checkbox.type = 'checkbox';
checkbox.className = 'todoCheckbox';
checkbox.id = `todoCheckbox${checkboxNumber}`;
if (isDone) {
checkbox.checked = true;
todoCard.className = 'doneCard';
} else {
todoCard.className = 'todoCard';
}
todoContent.className = 'todoContent';
todoContent.htmlFor = `todoCheckbox${checkboxNumber++}`;
todoContent.textContent = todo;
todoDelete.className = 'todoDelete';
todoDelete.textContent = 'X';
Copy link

Choose a reason for hiding this comment

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

이부분은 버튼 역할인 듯 한데 button 태그를 쓰면 좀더 편했을거 같아요!


todoCard.append(checkbox, todoContent, todoDelete);
if (isDone) {
document.querySelector('.doneSection').append(todoCard);
} else {
document.querySelector('.todoSection').append(todoCard);
Comment on lines +30 to +33
Copy link
Member

Choose a reason for hiding this comment

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

todoSection이나 doneSection에 document.querySelector를 실행하여 매번 DOM 요소에 접근하고 있는 것 같아서, 이는 성능저하에 큰 영향을 끼친다고 합니다!

Suggested change
if (isDone) {
document.querySelector('.doneSection').append(todoCard);
} else {
document.querySelector('.todoSection').append(todoCard);
const todoSection = document.querySelector('.todoSection');
const doneSection = document.querySelector('.doneSection');

이런식으로 DOM요소를 미리 찾아 변수를 저장해서 쓰는 것도 코드의 양과 성능 향상에 도움이 될 수 있을 것 같습니다.

}
};

//localstorage에 저장된 todo,done 불러오기
const todoList = JSON.parse(localStorage.getItem('todo'));
const donelist = JSON.parse(localStorage.getItem('done'));
Copy link
Member

@ddhelop ddhelop Mar 17, 2024

Choose a reason for hiding this comment

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

변수명은 목적은 잘 설명하고 있으나, camelCase를 사용하여 'doneList'로 사용하면 더 깔끔할 것 같아요.

Choose a reason for hiding this comment

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

여기서 localStorage.getItem('todo')와 localStorage.getItem('done') 이 null인 경우를 방지하기 위해 이런식으로 코드 리팩토링하셔도 좋을 것 같습니다~

Suggested change
const donelist = JSON.parse(localStorage.getItem('done'));
const doneList = JSON.parse(localStorage.getItem('done'), '[]');

if (todoList) {
todoList.forEach((todo) => {
createTodoCard(todo, false);
});
}
if (donelist) {
donelist.forEach((todo) => {
createTodoCard(todo, true);
});
}

//localstorage에 todo,done 추가하기
const addTodoToLocalStorage = (todo) => {
const todoList = JSON.parse(localStorage.getItem('todo'));
localStorage.removeItem('todo');
if (todoList) {
const newTodoList = [...todoList, todo];
localStorage.setItem('todo', JSON.stringify(newTodoList));
} else {
localStorage.setItem('todo', JSON.stringify([todo]));
}
};
const addDoneToLocalStorage = (todo) => {
const doneList = JSON.parse(localStorage.getItem('done'));
localStorage.removeItem('done');
if (doneList) {
const newDoneList = [...doneList, todo];
localStorage.setItem('done', JSON.stringify(newDoneList));
} else {
localStorage.setItem('done', JSON.stringify([todo]));
}
};

Comment on lines +52 to +72
Copy link
Member

Choose a reason for hiding this comment

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

addTodoToLocalStorage 함수와 addDoneToLocalStorage함수 그리고 deleteTodoFromLocalStorage와 deleteDoneFromLocalStorage 함수가 매우 유사한거 같아요. 공통 로직을 하나로 합치는 함수를 작성해보면 코드 가독성이나 유지보수에 정말 좋을 것 같습니다.

Suggested change
const addTodoToLocalStorage = (todo) => {
const todoList = JSON.parse(localStorage.getItem('todo'));
localStorage.removeItem('todo');
if (todoList) {
const newTodoList = [...todoList, todo];
localStorage.setItem('todo', JSON.stringify(newTodoList));
} else {
localStorage.setItem('todo', JSON.stringify([todo]));
}
};
const addDoneToLocalStorage = (todo) => {
const doneList = JSON.parse(localStorage.getItem('done'));
localStorage.removeItem('done');
if (doneList) {
const newDoneList = [...doneList, todo];
localStorage.setItem('done', JSON.stringify(newDoneList));
} else {
localStorage.setItem('done', JSON.stringify([todo]));
}
};
const updateLocalStorageList = (key, todo, isDelete = false) => {
let list = JSON.parse(localStorage.getItem(key)) || [];
if (isDelete) {
list = list.filter(item => item !== todo);
} else {
list.push(todo);
}
localStorage.setItem(key, JSON.stringify(list));
};

예를 들면 이런식으로 함수를 수정하면 코드의 양을 줄일 수 있을 것 같아요 :)

//localstorage에 todo,done 삭제하기
const deleteTodoFromLocalStorage = (todo) => {
const todoList = JSON.parse(localStorage.getItem('todo'));
const newTodoList = todoList.filter((item) => item !== todo);
localStorage.setItem('todo', JSON.stringify(newTodoList));
};
const deleteDoneFromLocalStorage = (todo) => {
const doneList = JSON.parse(localStorage.getItem('done'));
const newDoneList = doneList.filter((item) => item !== todo);
localStorage.setItem('done', JSON.stringify(newDoneList));
};

Choose a reason for hiding this comment

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

deleteTodoFromLocalStoragedeleteDoneFromLocalStorage 로직에 겹치는 부분이 많은 것 같아서 다음과 같이 함수를 하나로 합쳐도 좋을 것 같습니다:)

Suggested change
const deleteFunc = (todo, listName) => {
const itemList = JSON.parse(localStorage.getItem(listName));
const updatedList = itemList.filter((item) => item !== todo);
localStorage.setItem(listName, JSON.stringify(updatedList));
};

/*
doneCount: 완료 된 todo 개수
todoCount: 등록 된 todo 개수
*/

let doneCount = document.querySelectorAll('.doneCard').length;
let todoCount = document.querySelectorAll('.todoCard').length + doneCount;

//등록 된 todo, 완료 된 todo 개수 표시
const noteTodoProgress = () => {
Copy link

Choose a reason for hiding this comment

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

변수명 같은 경우에는 제 코드리뷰에 달린 부분이기도 한데, count가 들어가 있으면 좀더 직관적으로 이해할 수 있을거 같아요!

document.querySelector(
'.todoProgress'
).textContent = `${doneCount} 개 / ${todoCount} 개 `;
};
noteTodoProgress();

//오늘 날짜 표시
Copy link

Choose a reason for hiding this comment

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

이 함수가 제일 상단에 위치하거나 부수적인 부분이라 느껴지신다면 하단으로 가는게 가독성이 더 좋았을거 같아요!

const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const date = today.getDate();
const day = today.getDay();
const dayList = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
document.querySelector('.headerDate').append(`${date}`);
document.querySelector('.headerMonth').append(`${month}`);
document.querySelector('.headerYear').append(`${year}`);
document.querySelector('.headerDay').append(`${dayList[day]}`);
Comment on lines +101 to +119
Copy link
Member

Choose a reason for hiding this comment

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

오늘 날짜 표시하는 로직이 조금 긴 것 같습니다!!

Suggested change
//오늘 날짜 표시
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const date = today.getDate();
const day = today.getDay();
const dayList = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
document.querySelector('.headerDate').append(`${date}`);
document.querySelector('.headerMonth').append(`${month}`);
document.querySelector('.headerYear').append(`${year}`);
document.querySelector('.headerDay').append(`${dayList[day]}`);
const today = new Date();
const dayList = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
function updateHeader(selector, content) {
document.querySelector(selector).append(content);
}
updateHeader('.headerDate', today.getDate());
updateHeader('.headerMonth', today.getMonth() + 1);
updateHeader('.headerYear', today.getFullYear());
updateHeader('.headerDay', dayList[today.getDay()]);

document.querySelector(...).append(...) 호출을 줄이고,
querySelector와 append를 결합하는 호출을 하나의 함수로 만들어 재사용하면 더 가독성이 좋아질 것 같습니다 !!


//개수 업데이트 함수
const updateTodoCount = () => {
doneCount = document.querySelectorAll('.doneCard').length;
todoCount = document.querySelectorAll('.todoCard').length + doneCount;
noteTodoProgress();
};

//todo 추가하기
const todoInput = document.querySelector('.todoInput');

const pushNewTodo = () => {
if (todoInput.value.trim()) {

Choose a reason for hiding this comment

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

trim() 으로 공백 처리해주셨네요 👍🏻

createTodoCard(todoInput.value, false);

addTodoToLocalStorage(todoInput.value);
todoInput.value = '';
updateTodoCount();
} else {
todoInput.value = '';
alert('할 일을 입력해주세요!');
}
};

todoInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
pushNewTodo();
}
});

document.querySelector('.addTodo').addEventListener('click', pushNewTodo);

//todo 삭제하기
document.querySelector('.todoSection').addEventListener('click', (e) => {
if (e.target.className === 'todoDelete') {
e.target.parentElement.remove();
updateTodoCount();
deleteTodoFromLocalStorage(e.target.previousElementSibling.textContent);
}
});

document.querySelector('.doneSection').addEventListener('click', (e) => {
if (e.target.className === 'todoDelete') {
e.target.parentElement.remove();
updateTodoCount();
deleteDoneFromLocalStorage(e.target.previousElementSibling.textContent);
Copy link

Choose a reason for hiding this comment

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

previousElementSibling에 대해 알아갑니다! 리액트의 편리함을 다시 느끼네요..

}
});

//완료 todo 옮기기
document.querySelector('.todoSection').addEventListener('click', (e) => {
if (e.target.className === 'todoCheckbox') {
const todoCard = e.target.parentElement;
todoCard.className = 'doneCard';
document.querySelector('.doneSection').append(todoCard);
updateTodoCount();
deleteTodoFromLocalStorage(e.target.nextElementSibling.textContent);
addDoneToLocalStorage(e.target.nextElementSibling.textContent);
}
});
document.querySelector('.doneSection').addEventListener('click', (e) => {
if (e.target.className === 'todoCheckbox') {
const doneCard = e.target.parentElement;
doneCard.className = 'todoCard';
document.querySelector('.todoSection').append(doneCard);
updateTodoCount();
deleteDoneFromLocalStorage(e.target.nextElementSibling.textContent);
addTodoToLocalStorage(e.target.nextElementSibling.textContent);
}
});
Loading