-
Notifications
You must be signed in to change notification settings - Fork 11
[1주차] 최서연 미션 제출합니다. #5
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
base: main
Are you sure you want to change the base?
Conversation
현재 배포 링크에 들어가 보면, Access Required라는 문구가 뜨는데 아래에 첨부 드리는 참고 자료 보시고 모두가 멋진 과제물 볼 수 있도록 setting에서 변경 부탁드려요! 과제 하시느라 수고 많으셨어요 🩷 |
수정했습니다! 알려주셔서 감사합니다~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
20기 운영진을 했던 19기 출신 김승완이라고 합니다! 과제 진행하시느라 고생 정말 많으셨고, 코드에서 전체적인 HTML과 DOM 등에 대한 이해가 높으시고 고민 많이 하신 것이 보였습니다
동기분들 코드 리뷰도 열심히 해주시고, 리뷰를 받은 것에 대해 항상 리팩터링까지 해보는 경험을 하셨으면 좋겠습니다 :)
style.css
Outdated
.countIncompleteTodo { | ||
color: #AB4E59; | ||
font-size: 16px; | ||
} | ||
|
||
#todoContainer { | ||
display: flex; | ||
flex-direction: column; | ||
flex-grow: 1; | ||
height: 100%; | ||
width: 40rem; | ||
padding: 1.25rem; | ||
gap: .75rem; | ||
font-size: 1.25rem; | ||
} | ||
|
||
.todoSection { | ||
display: flex; | ||
width: 100%; | ||
gap: .5rem; | ||
} | ||
|
||
.todoInput { | ||
border: solid #AB4E59 .125rem; | ||
border-radius: .25rem; | ||
width: 100%; | ||
padding: 0 .25rem; | ||
} | ||
|
||
.todoInput::placeholder { | ||
color:#c49399; | ||
} | ||
|
||
.addButton { | ||
border: solid #AB4E59 .125rem; | ||
border-radius: .25rem; | ||
background-color: white; | ||
color: #AB4E59; | ||
padding: .25rem .5rem; | ||
cursor: pointer; | ||
flex-shrink: 0; | ||
font-size: 1rem; | ||
} | ||
|
||
.addButton:hover { | ||
background-color: #d6aab0; | ||
} | ||
|
||
.addButton:disabled { | ||
background-color: white; | ||
border: solid #d6aab0 .125rem; | ||
color: #d6aab0; | ||
cursor: default; | ||
} | ||
|
||
.deleteButton { | ||
width: 1.5rem; | ||
height: 1.5rem; | ||
background-size: contain; | ||
background-color: transparent; | ||
background-repeat: no-repeat; | ||
background-image: url('./assets/imgs/DeleteIcon.svg'); | ||
border: none; | ||
cursor: pointer; | ||
flex-shrink: 0; | ||
} | ||
|
||
.todoMain { | ||
min-height: 100%; | ||
} | ||
|
||
.todoCheckbox { | ||
appearance: none; | ||
width: 1.25rem; | ||
height: 1.25rem; | ||
border-radius: .25rem; | ||
border: solid #AB4E59 .125rem; | ||
background-color: white; | ||
cursor: pointer; | ||
flex-shrink: 0; | ||
} | ||
|
||
.todoCheckbox:checked { | ||
border: solid #AB4E59 .125rem; | ||
background-image: url('./assets/imgs/CheckIcon.svg'); | ||
background-size: contain; | ||
padding: .25rem; | ||
} | ||
|
||
.todoContent { | ||
display: block; | ||
min-width: 0; | ||
overflow-wrap: break-word; | ||
white-space: normal; | ||
} | ||
|
||
.completed { | ||
color: #d6aab0; | ||
text-decoration: line-through; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
브라우저마다 다른 css 정책, content-box 등의 레이아웃 속성을 일관적으로 통일하기 위해서 reset.css, normalize.css 등의 방식을 찾아보셔도 좋을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
찾아보겠습니다 감사합니다~!
utils/formatDate.js
Outdated
export const formatDate = (dayOffset = 0) => { | ||
const date = new Date(); | ||
date.setDate(date.getDate() + dayOffset); | ||
const options = { year: "numeric", month: "long", day: "numeric", weekday: "long" }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 코드는 함수 바깥으로 FORMAT_OPTIONS
와 같은 상수로 빼 두고, 내부적으로 사용하는 것도 괜찮아요. 나중에 날짜 표기 정책이 바뀌었을 경우에 해당 함수를 전부 뜯어보지 않고 option만 변경하면 되니까요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그렇게 생각하니 상수로 빼면 좋을 것 같네요. 다른 util 함수 작성할때도 적용하면 좋을 것 같아요. 수정하고 앞으로도 해당 방식 참고하겠습니다!
const today = formatDate(); | ||
|
||
document.addEventListener("DOMContentLoaded", () => { | ||
let selectedOffset = 0; | ||
let selectedDate = today; | ||
|
||
date.innerHTML = selectedDate; | ||
previousDate.addEventListener("click", () => { | ||
selectedOffset--; | ||
selectedDate = formatDate(selectedOffset); | ||
date.innerHTML = selectedDate; | ||
renderTodo(); | ||
}) | ||
|
||
nextDate.addEventListener("click", () => { | ||
selectedOffset++; | ||
selectedDate = formatDate(selectedOffset); | ||
date.innerHTML = selectedDate; | ||
renderTodo(); | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 코드를 보면 today 변수를 초기 값으로, selectedDate와 selectedOffset을 일종의 상태로 가지고 있어요. react나 vueJS 같은 라이브러리를 이용해보셨다면, 프론트엔드 기술에서 쓰이는 상태 개념과 굉장히 유사하다는 것을 알 수 있어요.
게다가 서연님은 이벤트가 발생했을 때 특정 컨텐츠에 대한 리렌더링을 핸들러 함수를 통해 잘 해주시고 계신데, spa 기술의 편리함을 느껴보시면 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어차피 selectedOffset을 기반으로 바로 selectedDate를 바로바로 만들어내는 것이라면 전체 코드 단에서 selectedDate라는 변수를 없애고 대체하면 메모리를 조금 아낄 수 있는데, 코드의 가독성 측면에서 변수를 이용하고 말고는 잘 판단하시면 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
하단 코드를 보시면 selectedDate를 계속 사용하게 되어서 변수로 따로 지정해주었습니다! 하지만 메모리 생각은 못했네요. 코드 작성할때 참고하겠습니다!
todoInput.addEventListener("keydown", (e) => { | ||
if(e.key === "Enter" && e.isComposing === false) { | ||
e.preventDefault(); | ||
todoButton.click(); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용자가 키보드로 인터랙션하는 경우를 잘 이해하고 계신 것 같아요.
프론트엔드 개발자는 form 관련 요소를 다룰 일이 정말 많은데 관련 이벤트도 공부해보시면 좋겠습니다.
저는 전체적으로 onKeydown -> onInput -> onKeyup -> onChange 순으로 input 요소의 경우에는 흐름이 이어지는 것으로 알고 있는데 찾아보시는 것을 추천드려요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다! 더 찾아보고 공부해보겠습니다😊
main.js
Outdated
const countTodo = () => { | ||
const incompleteTodos = todos[selectedDate].filter(todo => !todo.completed); | ||
countIncompleteTodo.innerHTML = "할 일: " + incompleteTodos.length; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아예 countTodo()
함수를 renderTodo()
등의 함수에서 사용하고 있는 만큼 정의부를 해당 블록의 상단으로 올려도 되지 않을까 싶어요. 전체적인 애플리케이션 동작에는 문제가 없는 것 같긴한데, 브라우저나 다양한 Javascript 실행환경에 따라 함수의 경우 표현식으로 작성하면 선언만 끌어올려지고 정의는 그대로인 경우가 있거든요.
Javascript 호이스팅과 관련된 실행 컨텍스트, 그리고 변수와 함수의 호이스팅 차이, TDZ 개념을 공부해보시면 좋을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수정하겠습니다! 말씀해주신 내용 더 공부해보겠습니다ㅎㅎ
style.css
Outdated
align-items: center; | ||
gap: .25rem; | ||
align-items: start; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
align-items
코드가 중복되므로 위에 있는 것은 없애주시는 것이 나을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헉 중복 작성한 줄 몰랐네요 수정하겠습니다!
const renderTodo = () => { | ||
todoList.innerHTML = ''; | ||
if (!todos[selectedDate]) { | ||
todos[selectedDate] = []; | ||
} | ||
todos[selectedDate].forEach((todo) => renderTodoItem(todo)); | ||
countTodo(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<article id="todoContainer"> | ||
<span class="countIncompleteTodo"></span> | ||
<section class="todoSection"> | ||
<input type="text" class="todoInput" placeholder="할 일을 입력해주세요."/> | ||
<button type="button" class="addButton">추가</button> | ||
</section> | ||
<main class="todoMain"> | ||
<ul class="todoList"></ul> | ||
</main> | ||
</article> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
시맨틱 태그를 잘 사용해서 전체적인 개요를 이해하기에 굉장히 쉬웠던 것 같아요! article태그는 독립적인 콘텐츠를 뜻한다고 하더라고요.
article로 section과 main을 감싸기 보다는 전체적인 할일을 의미하는 main 내부에서 article과 li를 함께 사용해서 리스트의 정보를 정리한다면 어떨까 싶습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다💕💕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
깔끔한 코드 구조와 함수 이름으로 유지보수하기 좋을 것 같아요! 또한 모듈 형식으로 해서 임포트를 할 수 있게 만드신게 인상적이에요. 또 폰트랑 이미지까지 적용하셨다니.. 정성 대박
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatDate()를 사용해 날짜 관련 로직을 별도의 유틸 함수로 분리한 점이 좋은것 같아요!
const today = formatDate(); | ||
|
||
document.addEventListener("DOMContentLoaded", () => { | ||
let selectedOffset = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이걸 사용해서 날짜 이동을 관리하는 점이 직관적인 것 같아요.
} | ||
|
||
// todo 추가 버튼 disable | ||
const disableAddButton = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력값이 비어 있으면 버튼을 비활성화하는 로직이 불필요한 클릭을 방지하는 데 효과적인것 같아요!! ux를 많이 고민해보신게 코드로 드러나네요
let selectedOffset = 0; | ||
let selectedDate = today; | ||
|
||
date.innerHTML = selectedDate; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inner html대신 textContent를 사용하시는게 어떨까요? innerHTML을 사용하면 XSS 위험이 있을 수도 있는데, textContent로 바꾸는 게 더 안전할 것 같아요!
} | ||
|
||
const renderTodo = () => { | ||
todoList.innerHTML = ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todoList.innerHTML = '' 대신 while (todoList.firstChild) { todoList.removeChild(todoList.firstChild); }를 사용하는것도 괜찮을 것 같아요! 저도 todoList.innerHTML = ''방식으로 구현했었는데 오늘 찾아보니 innerHTML = ''은 브라우저가 전체 DOM을 다시 파싱해야 하므로 성능상 약간의 부담이 있을 수 있다고도 합니다!
배포 링크
Todo List
배운 점 & 느낀 점
바닐라 js로 정말 간단한 프로젝트만 해보고 바로 React를 배운 입장에서 역시 바닐라 js로 과제를 하게 될 때마다 React의 소중함을 느끼게 됩니다...
한글의 경우 자음과 모음의 조합으로 한 음절이 만들어지는 조합 문자이기 때문에 글자가 조합 중인지, 조합이 끝난 상태인지를 알 수 없어서 Chrome의 경우 엔터로 todo를 추가할 때 두 번 추가가 되는 오류가 있었습니다. 해당 오류는 isComposing이라는 속성을 활용하여 해결할 수 있었습니다. 해당 속성은 입력 문자가 조합 문자인지 아닌지를 판단하여 boolean값으로 반환하므로 이벤트의 isComposing 속성이 false일 때만 이벤트를 처리하면 해당 오류를 해결할 수 있습니다.
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/isComposing
예전에 maxLength 속성을 이용하여 글자수 제한을 했을 때도 한글에서 오류가 났던 경험이 있어 데자뷰를 느꼈습니다...
매번 느끼지만 CSS 관련해서는 오류가 너무 많아서 많은 상황에서 테스트를 해봐야하는 것 같습니다. 마지막으로 테스트를 해보다가 아주 긴 문장이나 긴 단어를 입력했을 때 CSS가 깨지길래 급하게 수정했습니다. CSS는 정말 재밌지만 한편으로는 제일 어려운 부분이기도 한 것 같아요!
Key Questions
1. DOM은 무엇인가요?
DOM은 Document Object Model(문서 객체 모델)의 약자로, XML, HTML 같은 문서 구조를 객체로 표현한 것입니다.
W3C DOM 표준은 아래와 같은 세 가지 모델로 구분됩니다.
일반적으로 웹개발에서 DOM은 HTML DOM을 뜻합니다. HTML DOM(이하 DOM으로 표현)은 HTML 요소의 계층을 반영해서 만든 객체로, HTML로 구성된 웹 페이지와 스크립트 및 프로그래밍 언어를 연결시켜주는 역할을 합니다.
DOM의 계층은 트리 자료 구조로 구축되어 하나의 최상위 노드에서 다른 자식 노드를이 뻗어나가는 구조입니다.

참고로 HTML이 파싱을 거치면 DOM 트리, CSS가 파싱을 거치면 CSSOM 트리, Javascript가 파싱을 거치면 AST(Abstract Syntax Tree)가 되는데, DOM과 CSSOM은 합쳐져 렌더트리를 형성한다고 합니다. 여기서 렌더트리는 렌더링을 위한 트리 자료구조로, 이를 통해 렌더링이 실행됩니다.
JavaScript로
document.getElementById()
같은 메서드를 사용해 DOM을 직접 조작할 수 있습니다. 하지만 DOM을 직접 조작하면 속도가 느려질 수 있습니다. React는 UI를 효율적으로 업데이트하기 위해 Virtual DOM을 사용하여 메모리에서 가상으로 DOM을 관리합니다. 변경이 필요한 부분만 비교 후 실제 DOM을 최소한으로 업데이트해서 성능을 최적화합니다.한편, 브라우저는 동기적으로 HTML과 JavaScript를 처리하기 때문에
<script>
태그의 위치에 따라 DOM 생성이 느려질 수 있고, 스크립트에 DOM을 접근하는 코드가 있다면 제대로 실행이 되지 않을 수도 있습니다. 따라서<script>
태그는 HTML 문서 맨 하단에 넣거나 defer 속성을 추가하여 페이지가 모두 로드된 후에 해당 외부 스크립트가 실행됨을 명시해야합니다. 저는<script>
태그를 하단에 넣고 defer 속성도 추가하긴 했습니다.결론적으로, DOM은 HTML 문서를 브라우저가 이해할 수 있도록 트리 구조로 변환한 것이며, 이를 효율적으로 조작하고 최적화하는 방식은 웹 성능에 큰 영향을 미칩니다.
2. 이벤트 흐름 제어(버블링 & 캡처링)이 무엇인가요?
표준 DOM 이벤트에서 정의한 이벤트 흐름엔 다음 3가지 단계가 있습니다.
공식적으론 총 3개의 이벤트 흐름이 있지만, 이벤트가 실제 타깃 요소에 전달되는 단계인 타깃 단계는 별도로 처리되지 않습니다.
또한 on 프로퍼티나 HTML 속성, addEventListener(event, handler)를 이용해 할당된 핸들러는 타깃 단계 혹은 버블링 단계에서만 동작하기 때문에 캡처링에 대해 전혀 알 수 없습니다. 캡처링 단계에서 이벤트를 잡아내려면 addEventListener의 capture 옵션을 true로 설정해야 합니다. capture 옵션이 false(default)이면 핸들러는 버블링 단계에서 동작하고, true이면 핸들러는 캡처링 단계에서 동작합니다.
<td>
를 클릭하면 이벤트가 최상위 조상에서 시작해 아래로 전파되고(캡처링 단계), 이벤트가 타깃 요소에 도착해 실행된 후(타깃 단계), 다시 위로 전파됩니다(버블링 단계).window
→document
→html
→body
→table
→tbody
→tr
→td
(캡처링 단계)td
(타깃 단계, 캡처링 단계의 옵션을 true로 설정했으면 두 번 호출 됨)td
→tr
→tbody
→table
→body
→html
→document
→window
(버블링 단계)window 객체를 만날 때까지, 각 요소에 할당된 onclick 핸들러가 동작하는데, 이런 흐름을 '이벤트 버블링’이라고 부릅니다. 이벤트가 제일 깊은 곳에 있는 요소에서 시작해 부모 요소를 거슬러 올라가며 발생하는 모양이 마치 물속 거품(bubble)과 닮았기 때문입니다. 버블링의 특성을 통해 우리는 그 부모 요소에 이벤트를 등록하면, 어떤 자식 요소를 클릭하든 부모 요소로 이벤트가 전파되기 때문에 간단하게 원하는 구현을 할 수 있습니다. 이 기법을 이벤트 위임이라 부릅니다.
focus 이벤트와 같은 몇몇 이벤트를 제외하곤 대부분의 이벤트는 버블링됩니다.
event.stopPropagation()
를 통해 버블링을 막을 수 있지만,event.stopPropagation()
를 사용한 영역은 죽은 영역(Dead Zone)이 되어버리기 때문에 권장되지 않습니다. 불가피하게 버블링을 막아야한다면, 해당 메서드 대신 커스텀 이벤트 등을 사용해 이벤트 버블링을 통제하는 것이 권장됩니다.3. 클로저와 스코프가 무엇인가요?
스코프(Scope, 유효 범위) 는 참조 대상 식별자(identifier)를 찾아내기 위한 규칙으로, 자바스크립트는 이 규칙대로 식별자를 찾습니다.
스코프는 다음과 같이 두 가지로 구분됩니다.
변수의 선언 위치가 전역이라면 전역 스코프를 갖는 전역 변수가 되고, 선언 위치가 지역(함수나 블록 내부 등)이라면 지역 스코프를 갖는 지역 변수가 됩니다. 여기서 블록 레벨 스코프는 let과 const를 사용했을 때 적용되는 스코프 개념입니다.
어휘적 스코프(Lexical Scope)는 함수를 어디에 선언하였는지에 따라 상위 스코프가 결정되는 것을 뜻합니다. 자바스크립트를 포함한 대부분의 프로그래밍 언어는 렉시컬 스코프를 따르며, 이를 정적 스코프(Static Scope)라고 부르기도 합니다. 렉시컬 스코프를 따르는 자바스크립트 엔진은 코드를 실행하기 전에 렉시컬 환경을 먼저 분석합니다. 참고로, 동적 스코프는 렉시컬 스코프와 다르게 함수를 어디서 호출하였는지에 따라 상위 스코프가 결정되는 것을 뜻합니다. Bash Scripting, Common Lisp 등 일부 언어에서 동적 스코프를 따른다고 합니다.
클로저(Closure) 는 함수와 함수가 선언된 어휘적 환경의 조합입니다. 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(렉시컬 스코프)인 스코프를 기억하여 자신이 선언됐을 때의 스코프 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수의 특성입니다. 즉, 클로저는 자신이 생성될 때의 환경인 렉시컬 스코프을 기억할 수 있도록 합니다.
자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없습니다. 하지만 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 합니다. 또한 클로저 함수는 외부 함수의 실행이 끝나더라도 외부 함수 내 변수를 사용할 수 있도록 합니다. 이렇듯 클로저는 특정 데이터를 스코프 안에 가두어 둔 채로 계속 사용할 수 있게하는 폐쇄성을 갖습니다.
자바스크립트에서도 클래스 기반 언어의 public과 private을 흉내내어 변수에 대한 접근 권한을 제어하여 정보 은닉이 가능합니다.
또한 클로저 함수를 각각의 변수에 할당하면 각자 독립적으로 값을 사용하고 보존할 수 있습니다. 함수의 재사용성을 극대화 함수 하나를 독립적인 부품의 형태로 분리하는 것을 모듈화라고 하는데, 클로저를 통해 데이터와 메소드를 묶어다닐 수 있기에 클로저는 모듈화에 유리합니다.
클로저를 통해 자바스크립트 개발자는 외부 스코프에서 선언된 변수를 내부 함수에서 접근할 수 있게 되고, 이는 정보의 접근 제한(캡슐화)를 가능하게 하며, 모듈 패턴의 구현 등 다양한 프로그래밍 패턴에서 활용됩니다.