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주차] 정인영 미션 제출합니다. #7

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
49 changes: 17 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
# 1주차 미션: Vanilla-Todo

# 서론

안녕하세요 🙌🏻 18기 프론트엔드 운영진 **배성준**입니다.

이번 미션은 개발 환경 구축과 스터디 진행 방식에 익숙해지실 수 있도록 간단한 **to-do list** 만들기를 진행합니다. 무작정 첫 스터디부터 React를 다루는 것보다는 왜 React가 필요한지, React가 없으면 무엇이 불편한지 느껴 보고 본격적인 스터디에 들어가는 것이 React를 이해하는 데 더 많은 도움이 될 것이라 생각합니다.

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

막히는 부분이 있더라도 우선은 스스로 공부하고 찾아보는 방법을 권고드리지만, 운영진의 도움이 필요하시다면 얼마든지 슬랙 Q&A 채널이나 프론트엔드 카톡방에 질문을 남겨 주세요!

# 미션

## 미션 목표

Expand All @@ -26,33 +15,29 @@
## Key Questions

- DOM은 무엇인가요?

DOM은 문서 객체 모델의 약어인데, 브라우저가 웹 페이지를 인식하는 방식을 트리 구조로 계층화시켜 프로그래밍 방식으로 조작하게 해주는 API입니다. 트리 구조의 최상단에는 document 객체가 있고, 이 객체를 통하여 DOM 트리의 여러 노드에 접근할 수 있습니다. 이번 과제에서는 Vanilla JS를 통하여 DOM을 조작하여 클릭, 엔터와 같은 이벤트를 처리했습니다.
- HTML (tag) Element를 JavaScript로 생성하는 방법은 어떤 것이 있고, 어떤 방법이 가장 적합할까요?

이번 과제에 Element를 생성할 때 사용한 DOM API는 .createElement()였는데, innerHTML()과 insertAdjacent() 와 비교했을 때 생성한 요소를 삽입하는 추가 코드가 필요하여 번거로운 부분이 있으나 이번 과제에서 DOM 트리 구조가 그렇게 복잡하지 않아 사용하였습니다.
하지만 이런 간단한 작업에도 상대적으로 코드가 길어져, 아무래도 일반적인 프로젝트에서는 리액트, 뷰 같은 라이브러리를 사용하는 게 좋을 것 같습니다.

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

Semantic tag는 "의미가 있는 태그" 라는 뜻으로 header, nav, main, footer, article, section 등이 있습니다. 이러한 시멘틱 태그를 적절히 사용하면, 키워드를 통한 검색결과에 잘 노출되게 할 수 있고, 페이지 탐색에 도움을 줄 수 있습니다.
개인적으로 느꼈던 시멘틱 태그의 큰 장점은 html의 구조를 설계할 때, 시멘틱 태그를 통해 설계하면 좀 더 코드 가독성이 늘었던 거 같습니다.


- Flexbox Layout은 무엇이며, 어떻게 사용하나요?
- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요?
- 코드에서 주석을 다는 바람직한 방법은 무엇일까요?

## 필수 요건
Flexbox layout 은 요소를 가로 정렬할 때는 물론 요소의 배치와 정렬하는 데 사용되는 CSS 레이아웃 모델입니다.
기본적으로 정렬할 요소의 부모 요소에다가 display: flex 를 부여해주어 Flexbox layout을 개시하게 됩니다. 기호에 따라 부모 요소에 justify-content, align-items, flex-wrap 와 같은 속성들을 정의해주고, 자식 요소에선 align-self, flex-grow 와 같은 속성들을 정의하여 정렬을 반응형으로 어렵지 않게 구현하는데 정말 유용하게 쓰이는 것 같습니다.
- JavaScript가 다른 언어들에 비해 주목할 만한 점에는 어떤 것들이 있나요?

- [결과 화면](https://vanilla-todo-17th-qras.vercel.app/)의 기능을 그대로 구현합니다.
- 결과 링크의 화면 디자인 그대로 구현해도 좋고, 자신만의 디자인을 적용해도 좋습니다.
- CSS의 Flexbox를 이용하여 레이아웃을 구성합니다.
- JQuery, React, Bootstrap 등 외부 라이브러리를 사용하지 않습니다.
- 함수와 변수의 이름은 lowerCamelCase로 짓습니다.
- 코딩의 단위를 기능별로 나누어 Commit 메세지를 작성합니다.
이번에 학교 수업에서 프론트, 백엔드, DB를 모두 자바스크립트만으로 구현하는 법을 배우고 있는데 (React, Express, MySQL) 이처럼 하나의 언어로 모든 분야를 조작할 수 있다는 점에서 자바스크립트는 굉장히 자유도가 높으며 활용도가 높은 언어인 것 같고, 앞으로도 수많은 프레임워크와 라이브러리가 나올 것 같습니다.
- 코드에서 주석을 다는 바람직한 방법은 무엇일까요?

## 선택 요건
코드가 복잡할 때 주석을 많이 달기 보다는, 그럴 때 일수록 핵심적인 부분만 주석을 다는 게 좋은 것 같습니다.

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

# 링크 및 참고자료

- [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/)
Binary file added images/blackhole.gif
Copy link

Choose a reason for hiding this comment

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

엄청 강렬한 이미지네요ㅋㅋㅋ 시선을 확 끕니다

Copy link
Author

@rmdnps10 rmdnps10 Sep 16, 2023

Choose a reason for hiding this comment

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

감사합니다 😊 giphy에서 긁어왔어요!

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions images/galaxy-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/giphy.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/restore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/rock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 42 additions & 5 deletions index.html
Copy link

Choose a reason for hiding this comment

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

semantic tag를 사용하여 페이지를 구성해서 훨씬 가독성이 좋은거 같습니다! 저도 다음에는 사용해보겠습니다 ㅎㅎ

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,49 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Todo</title>
<title>Universe-Todo</title>
<link rel="stylesheet" href="reset.css" />
<link rel="stylesheet" href="style.css" />
<script defer src="./script.js"></script>
</head>

<body>
<div class="container"></div>
<header>
<div class="input-section">
<div class="icon">
<img src="./images/galaxy-icon.svg" alt="아이콘" />
</div>
<form class="todo">
<input
type="text"
placeholder="당신의 할 일을 별을 먹어치우는 블랙홀처럼 모두 해치우세요."
/>
</form>
<button>Enter</button>
Copy link

Choose a reason for hiding this comment

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

이 버튼을 form 안에 옮기면 click과 submit이 모두 submit으로 인식될거에요~

</div>
<div class="quote-section">Carl Seagan - " 사멸은 법칙이다. "</div>
</header>
<main>
<div class="todos-solves">
<div class="todo-section">
<h1>✔ 지금 당장 해결하세요.</h1>
<p class="caption">
총 <span class="todo-number"></span>개의 할 일이 있어요.
</p>
<ul class="todo-list">
<!-- <li>백준 실버5 2394, 2395번 풀기</li>
<li>개인 velog에 Recoil의 사용법에 대해서 포스팅 해보기</li> -->
</ul>
</div>
<div class="solved-section">
<h1>✔ 최근 해결한 내역</h1>
<p class="caption">당신이 해결한 일들은 이곳에 모두 저장됩니다.</p>
<ul class="solved-list"></ul>
</div>
</div>
<div class="blackhole-section">
<img src="./images/blackhole.gif" alt="블랙홀 이미지" />
</div>
</main>
<footer>Copyrightⓒ2023 ChungInYoung All rights reserved.</footer>
</body>
<script src="script.js"></script>
</html>
</html>
43 changes: 43 additions & 0 deletions reset.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
Copy link

Choose a reason for hiding this comment

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

reset.css를 사용하여 디폴트 스타일을 초기화한다는 개념을 처음 알았네요!! 한 수 배워갑니다👍

155 changes: 155 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
let currentTasks = [];
let completedTasks = [];
const maxCompletedTasksToShow = 5;

const taskForm = document.querySelector(".todo");
const taskList = document.querySelector(".todo-list");
const completedList = document.querySelector(".solved-list");
const submitButton = document.querySelector("button");

const resetInputText = () => {
const taskInput = document.querySelector(".todo > input");
taskInput.value = "";
};

const addTaskToList = (text) => {
if (!currentTasks.includes(text)) {
Copy link

Choose a reason for hiding this comment

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

이미 같은 일을 할 일에 적어두었을 때는 추가되지 않도록 해두었군요!!! 디테일이 너무 좋아요~~👍

Copy link

Choose a reason for hiding this comment

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

includes 메서드 사용해서 task의 중복이 없도록 예외처리한게 인상 깊습니다!

const listItem = document.createElement("li");
listItem.innerHTML = text;
currentTasks.push(text);
taskList.appendChild(listItem);
}
};

const addCompletedTaskToList = (text) => {
if (!completedTasks.includes(text)) {
const listItem = document.createElement("li");
const textElement = document.createElement("p");
textElement.innerHTML = text;
const deleteImg = document.createElement("img");
deleteImg.src = "./images/delete.png";
Copy link

Choose a reason for hiding this comment

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

앞으로 디자이너와 소통하면서 logic과 view를 최대한 분리시킨 상태로 코딩을 진행하는 것이 좀 더 편하실 거라고 생각해요!
그래서, view를 담당하는 코드는 최대한 css에 맡기고, js파일에는 logic 관련 코드만 두는게 디버깅하고 업데이트 하는 데에도 좋을거에요ㅎㅎ
img src같은 경우는 각각 class를 지정해서 미리 css에 선언해두면 보기에도 좋고 로딩도 조금 더 빠를 수 있을 것이라고 생각합니다!

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 것처럼 앞으로의 디자이너와의 협업 과정에서 logic과 view를 분리하는 것은 중요한 과정인 것 같습니다 ㅎㅎ 피드백 감사합니다!

const restoreImg = document.createElement("img");
restoreImg.src = "./images/restore.png";
listItem.appendChild(deleteImg);
listItem.appendChild(restoreImg);
listItem.appendChild(textElement);
Comment on lines +33 to +35
Copy link

Choose a reason for hiding this comment

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

저도 똑같이 피드백 받은 부분인데 append() 메서드를 사용해서 child를 한번에 append하는 방식을 사용해도 좋을 것 같아요~!

Suggested change
listItem.appendChild(deleteImg);
listItem.appendChild(restoreImg);
listItem.appendChild(textElement);
listItem.append(deleteImg, restoreImg, textElement);

completedTasks.push(text);
completedList.appendChild(listItem);
const filteredTasks = currentTasks.filter((task) => task !== text);
currentTasks = filteredTasks;
localStorage.setItem("currentTasks", JSON.stringify(currentTasks));
localStorage.setItem("completedTasks", JSON.stringify(completedTasks));
if (completedList.children.length > maxCompletedTasksToShow) {
Copy link

Choose a reason for hiding this comment

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

완료된 task의 수에 제한을 두는건 과도하게 데이터가 쌓이지 않도록 방지하는 차원에서 좋은 것 같습니다!
근데, 성능적으로 봤을 때, element를 만들어서 list에 추가하는 과정 이후에 개수를 체크하는 것보다 해당 함수가 실행 됐을 때,
머너 체크하는 것이 더 좋을 것 같아요! 추가하고 지우는거보다 미리 검사한 후에 추가하는 과정을 스킵하는게 더 효율적이지 않을까 싶습니다ㅎㅎ

Copy link
Author

@rmdnps10 rmdnps10 Sep 16, 2023

Choose a reason for hiding this comment

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

확실히 함수가 실행됐을 때 미리 체크하는게 성능적으로 봤을 때 훨씬 효율적일 것 같습니다!!

completedList.lastChild.remove();
Copy link

Choose a reason for hiding this comment

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

만약, 개수 검증을 하는걸 뒷쪽에 두는 것이 좀 더 의미있으려면, firstChild를 지워서 가장 일찍 list에 들어온 항목이 지워지고 새로운 항목이 추가되도록 하면 재밌을 것 같아요~~

Copy link
Author

Choose a reason for hiding this comment

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

사실 개수 검증 코드를 작성하려다가,, 뭔가 계속 꼬여서 그만두었는데 마음을 가다듬고 로직을 찬찬히 생각해보면서 좋았을거같네요.. 감사합니다!!

}
}
};

const loadTasks = (item) => {
const storedTasks = localStorage.getItem(item);
const parsedTasks = JSON.parse(storedTasks);
parsedTasks.forEach((task) => {
addTaskToList(task);
});
};

const loadCompletedTasks = (item) => {
const storedCompletedTasks = localStorage.getItem(item);
const parsedCompletedTasks = JSON.parse(storedCompletedTasks);
parsedCompletedTasks.forEach((completedTask) => {
Copy link

Choose a reason for hiding this comment

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

js의 iteration 방법을 잘 이용하신 것 같습니다~~

addCompletedTaskToList(completedTask);
});
};
Comment on lines +48 to +62
Copy link

Choose a reason for hiding this comment

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

코드 전체적으로 Task와 CompletedTask에 대한 함수로 기능이 확실하게 구분돼있어서 굉장히 좋습니다!


const updateTaskCount = () => {
const taskCount = document.querySelector(".todo-number");
taskCount.innerHTML = currentTasks.length;
};

const submitHandler = (event) => {
event.preventDefault();
const taskText = event.target.children[0].value;
if (taskText.length < 3) {
alert("3글자 이상 입력하세요.");
return;
} else if (taskText.length > 25) {
alert("25글자 이하로 요약해서 입력해주세요.");
return;
}
addTaskToList(taskText);
localStorage.setItem("currentTasks", JSON.stringify(currentTasks));
updateTaskCount();
resetInputText();
};

const clickHandler = (event) => {
Copy link

Choose a reason for hiding this comment

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

지금은 submit handler와 clickHandler를 따로 명시해서 작성해주셨는데, 같은 기능을 하는 코드를 따로 작성하는 것보다 하나로 작성하는게 효율적일 것 같아요! 아마, 지금 html 작성된 것을 보면

태그 바깥에 button이 위치해서, click과 enter가 서로 다르게 작동하는 것 같습니다

button을 form 태그 안에 위치시키면 submit event에 클릭과 엔터가 모두 반응할거에요~~ 그러면 클릭 이벤트 헨들러는 따로 작성하지 않아도 될 것 같습니다ㅎㅎ(그리고 button에 type='submit'을 지정하는게 좀 더 명시적으로 역할을 보여줄 수 있을 것 같아요!)

Copy link
Author

Choose a reason for hiding this comment

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

맞습니다... 확실히 html 구조를 어떻게 설계하냐에 따라 코드 로직 구조의 효율성이 많이 다른 것 같아요, 그리고 button에 type을 명시하는 것 ㅎㅎㅎ 감사합니다!

event.preventDefault();
const taskText = event.target.previousElementSibling.children[0].value;
if (taskText.length < 3) {
alert("3글자 이상 입력하세요.");
Copy link

Choose a reason for hiding this comment

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

글자 수를 이용해서 3글자 이상일 때, alert 창이 나오도록 해주셨는데,
input에 스페이스 바를 3번 누르고 엔터를 누르면 입력이 그대로 들어가요!
trim()을 이용해서 양쪽 공백을 없앤 뒤, 3글자 이상일 때, alert 창이 나오도록 해주면 좋을 것 같아요!!

return;
} else if (taskText.length > 25) {
alert("25글자 이하로 요약해서 입력해주세요.");
return;
}
addTaskToList(taskText);
localStorage.setItem("currentTasks", JSON.stringify(currentTasks));
updateTaskCount();
resetInputText();
};

const taskClickHandler = (event) => {
const filteredTasks = currentTasks.filter(
(task) => task !== event.target.innerHTML
);
addCompletedTaskToList(event.target.innerHTML);
currentTasks.length = 0;
currentTasks.push(...filteredTasks);
localStorage.setItem("currentTasks", JSON.stringify(currentTasks));
completedTasks.push(event.target.innerHTML);
localStorage.setItem("completedTasks", JSON.stringify(completedTasks));
event.target.remove();
updateTaskCount();
};

const completedTaskClickHandler = (event) => {
if (
event.target.tagName === "IMG" &&
event.target.src.includes("delete.png")
) {
const deletedTask =
event.target.nextElementSibling.nextElementSibling.innerHTML;
const filteredCompletedTasks = completedTasks.filter(
Copy link

Choose a reason for hiding this comment

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

filter method를 통해서 delete와 completed 구분하신 것 잘하신 것 같습니다~~

(task) => task !== deletedTask
);
completedTasks = filteredCompletedTasks;
console.log(completedTasks);
localStorage.setItem("completedTasks", JSON.stringify(completedTasks));
event.target.parentElement.remove();
} else if (
event.target.tagName == "IMG" &&
event.target.src.includes("restore.png")
) {
const restoredTask = event.target.nextElementSibling.innerHTML;
Copy link

Choose a reason for hiding this comment

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

event 객체를 통해서 dom element를 조작하는거 좋은 것 같습니다ㅎㅎ 저는 nextElementSibling 이 있는지 몰랐는데 앞으로 써먹어봐야겠네요!

const filteredCompletedTasks = completedTasks.filter(
(task) => task !== restoredTask
);
completedTasks = filteredCompletedTasks;
localStorage.setItem("completedTasks", JSON.stringify(completedTasks));
addTaskToList(restoredTask);
localStorage.setItem("currentTasks", JSON.stringify(currentTasks));
event.target.parentElement.remove();
updateTaskCount();
}
};

taskForm.addEventListener("submit", submitHandler);
submitButton.addEventListener("click", clickHandler);
taskList.addEventListener("click", taskClickHandler);
Copy link

Choose a reason for hiding this comment

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

화면 캡처 2023-09-17 133822

할 일을 한 가지를 정확하게 누르지 않고, 바깥을 누르면 위 사진과 같은 결과가 나와요!!
taskList에 전체적으로 이벤트를 준 것 때문인 것 같아요!!

completedList.addEventListener("click", completedTaskClickHandler);

(() => {
loadTasks("currentTasks");
loadCompletedTasks("completedTasks");
updateTaskCount();
})();
Loading