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주차] 최지원 미션 제출합니다. #3

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2174809
feat: 기본 UI 구현
jiwonnchoi Sep 5, 2024
3556734
feat: 입력창 토글 기능 구현
jiwonnchoi Sep 6, 2024
1434121
feat: 할 일 추가 함수 구현
jiwonnchoi Sep 6, 2024
8ec025d
feat: 토글, 삭제 함수 구현
jiwonnchoi Sep 6, 2024
e3313a3
feat: 날짜 및 현재시각 표기
jiwonnchoi Sep 6, 2024
a254567
feat: 로컬스토리지 데이터 처리 구현
jiwonnchoi Sep 6, 2024
3152ab0
feat: 반응형 구현
jiwonnchoi Sep 6, 2024
4de67aa
style: 클래스명 수정 및 rem 단위 변환
jiwonnchoi Sep 6, 2024
8f7e29b
feat: 공통 기능 함수화
jiwonnchoi Sep 6, 2024
c08b6fa
feat: 전체, 완료된 항목 개수 표기 추가
jiwonnchoi Sep 6, 2024
86260ea
feat: 입력창 애니메이션 추가
jiwonnchoi Sep 6, 2024
3d0989b
fix: 스타일 수정 및 주석 추가
jiwonnchoi Sep 7, 2024
ca4e76e
chore: 버셀 배포
jiwonnchoi Sep 7, 2024
3d044fe
refactor: 아이콘 png에서 svg로 변경
jiwonnchoi Sep 10, 2024
28d46e6
refactor: 현재시각 1초지연 방지
jiwonnchoi Sep 10, 2024
970222b
refactor: did 네이밍 done으로 변경
jiwonnchoi Sep 10, 2024
cd12e36
refactor: addTodoItem의 input값 캐싱
jiwonnchoi Sep 10, 2024
780ce6d
refactor: deleteItem 중복호출 수정
jiwonnchoi Sep 11, 2024
a8f23ca
refactor: 전역 스타일 추가
jiwonnchoi Sep 11, 2024
eb9f93e
refactor: toggleForm 로직 분리 및 불리언 추가
jiwonnchoi Sep 11, 2024
9ef7e0e
refactor: 버튼생성 이벤트를 상위에 위임하도록 수정
jiwonnchoi Sep 11, 2024
f6d8336
fix: 배열 각 항목에 고유id 추가
jiwonnchoi Sep 11, 2024
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: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h1 class="today"></h1>
<section class="todo">
<div class="todo-top">
<h2 class="ment-style">What I have to do</h2>
<form class="input-box box-style">
<form class="input-box box-style" style="display: none">
<input class="input" />
<button class="submit-btn">작성하기</button>
</form>
Expand Down
219 changes: 96 additions & 123 deletions script.js
Original file line number Diff line number Diff line change
@@ -1,172 +1,145 @@
// 현재 시간 업데이트
/* 날짜 및 현재 시각 */
const updateTime = () => {
const today = document.querySelector(".today");
const now = new Date();
today.innerHTML = now.toLocaleString();
};
setInterval(updateTime, 1000); // 1초마다 호출
Copy link
Member

Choose a reason for hiding this comment

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

호출 시 즉시 실행시키면 첫 1초를 기다리지 않아서 이런식으로 구성해도 좋을 것 같습니다.

Suggested change
setInterval(updateTime, 1000); // 1초마다 호출
updateTime();
setInterval(updateTime, 1000);

Copy link
Author

Choose a reason for hiding this comment

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

단순 로딩 문제라 생각했는데 앞으로는 쓰려는 함수의 작동을 정확히 이해하고 써야할 것 같습니다😂 감사합니다!


/* 로컬 스토리지 데이터 처리 */
/* localStorage 데이터 처리 */

// localStorage에 항목 저장하기 함수
const saveToLocalStorage = (key, data) => {
localStorage.setItem(key, JSON.stringify(data));
};

// localStorage의 항목 불러오기 함수
const loadFromLocalStorage = (key) => {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : [];
};

let todos = loadFromLocalStorage("todos");
let dones = loadFromLocalStorage("dones");
let todos = loadFromLocalStorage("todos"); // 할 일 배열
let dones = loadFromLocalStorage("dones"); // 한 일 배열
Comment on lines +36 to +37
Copy link
Member

Choose a reason for hiding this comment

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

todos와 dones는 배열을 변경할 필요가 없으므로, const를 사용해도 좋을 것같습니다! let은 값이 재할당되거나 변동될 가능성이 있는 경우에만 사용하는 것이 좋다고 합니다. :)

Copy link
Author

Choose a reason for hiding this comment

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

항목을 삭제하거나 옮기는 deleteTodoItem, todoToDid 등의 함수에서 값을 재할당하는 부분이 있어서 let을 사용하였습니다!

사실 코드 쓸 때는 배열이 계속해서 변동된다는 생각에 곧바로 let을 사용하고선 넘어갔는데, 주신 리뷰보고서 한 번 더 디버깅 해보면서 정확히 어디까진 동작하고 어느 지점부터 재할당으로 인해 반영이 안되는지 파악해볼 수 있었습니다.
또 push()도 배열을 바꾼다고 생각하여 let을 써야겠다는 생각에 한 몫하였는데, 다시 찾아보니 배열을 선언한 것은 포인터이므로 pop(), push()를 통한 접근은 객체가 저장된 공간이 아닌 그 안의 내용이기 때문에 const로 선언해도 무관하다는 내용을 정확히 정리하고 넘어갈 수 있었습니다.

Comment on lines +36 to +37
Copy link

Choose a reason for hiding this comment

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

로컬스토리지에 key값을 두 개로 분리하여 저장하는 방법을 배워갑니다! 호기심에 다른 방법은 없을까 찾아보다
let tasks = loadFromLocalStorage("tasks") || { todos: [], dones: [] }; // 초기화
로 하나의 키값에 배열만 따로 두어 관리하는 방법도 있음을 알게 되었습니다. 한번 참고해보셔도 좋을 것 같습니다


// localStorage의 항목으로 초기화하기 함수
const initTodoList = () => {
todos.forEach((todo) => printTodo(todo));
dones.forEach((done) => printDidItem(done));
todos.forEach((todo) => printItem(todo, "todo"));
dones.forEach((done) => printItem(done, "did"));
Copy link
Collaborator

Choose a reason for hiding this comment

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

완료된 항목을 나타내기 위한 표현으로 did와 done이 혼용되어있어서 하나로 통일하는 것이 좋지 않을까 해요!!
자칫 다른 사람이 보기에, done과 did가 다른 역할을 수행한다는 착각을 불러일으킬 수 있기 때문에,
did-text -> completed-text , dones -> completed 이런식으로 모든 관련 함수, 변수, 클래스에서 완료된 항목을 의미하는 용어를 일관되게 수정하는 것을 제안드려요!!

};

/* todo 입력, 토글, 삭제 */
const form = document.querySelector(".input-box");
const showMessage = document.querySelector(".show-input");

const init = () => {
form.addEventListener("submit", addTodoItem);
showMessage.addEventListener("click", toggleForm);
initTodoList(); // 초기 로드 시 로컬 스토리지 데이터 불러오기
/* 공용 함수 */
// 버튼 생성하기 함수
const createBtn = (src, className, clickHandler) => {
const btn = document.createElement("button");
const img = document.createElement("img");
img.setAttribute("src", src);
btn.appendChild(img);
btn.setAttribute("class", className);
btn.addEventListener("click", clickHandler);
Copy link
Collaborator

@jinnyleeis jinnyleeis Sep 8, 2024

Choose a reason for hiding this comment

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

각 버튼에 대해 개별적으로 이벤트 리스너를 추가하는 방식으로 작성해주셨는데,
버튼마다 하나하나 이벤트 리스너를 등록하기 보단, 이번에 키 퀘스천 주제였던 '이벤트 버블링'을 활용해 보는 것이 어떨까 제안드려요!
이벤트 버블링은 아시다시피 하위 요소에서 발생한 이벤트가 상위 요소로 전파되는 것이므로,
이를 통해 상위 요소가 하위 요소들의 이벤트를 한 번에 처리할 수 있어요.

때문에, 상위 요소에 이벤트 리스너를 등록하고, 실제 클릭된 하위 요소(버튼)는 event.target을 통해 받아와서 적절한 로직을 취하는 것을 제안드려요!

이 방식을 취하면, 버튼 클릭 처리 로직과 / 버튼 생성 createBtn , 항목 출력하기 printItem 로직을 분리할 수 있어서, 나중에 유지보수할때도 좋고, 코드의 가독성도 좋아질 것 같아요.

먼저 .todoList, .didList에 한 번만 이벤트 리스너를 등록한 후에,
didToTodo 함수에서 closest() 메서드를 활용해주신 것처럼, closest() 메서드를 통해 클릭된 하위 요소가 체크 버튼인지 삭제 버튼인지 확인 후에 이에 따른 적절한 완료처리/삭제처리 로직을 실행해주는 것이 좋을 것 같아요!

`수정 코드 첨부합니다!!
// 이벤트 위임을 통한 버튼 클릭 처리
document.querySelector('.todoList').addEventListener('click', (e) => {
if (e.target.closest('.todo-check')) {
todoToDid(e);
} else if (e.target.closest('.todo-del')) {
deleteTodoItem(e);
}
});

document.querySelector('.didList').addEventListener('click', (e) => {
if (e.target.closest('.todo-check')) {
didToTodo(e);
} else if (e.target.closest('.todo-del')) {
deleteDidItem(e);
}
});

// 버튼 생성하기 함수 (이벤트 리스너 없이 생성)
const createBtn = (src, className) => {
const btn = document.createElement("button");
const img = document.createElement("img");
img.setAttribute("src", src);
btn.appendChild(img);
btn.setAttribute("class", className);
return btn;
};

// 항목 출력하기 함수 (버튼에 이벤트 리스너 추가x)
const printItem = (text, type) => {
const list = document.querySelector(.${type}List);
const item = document.createElement("li");
const itemContent = document.createElement("div");
const itemText = document.createElement("span");
itemText.innerText = text;
itemText.className = ${type}-text;

// 체크 버튼 생성
const checkBtn = createBtn(
type === "todo" ? "images/empty_checkbox.png" : "images/full_checkbox.png",
"todo-check"
);

// 삭제 버튼 생성
const deleteBtn = createBtn("images/delete_btn.png", "todo-del");

itemContent.className = "todo-item";
itemContent.appendChild(checkBtn);
itemContent.appendChild(itemText);
itemContent.appendChild(deleteBtn);

item.appendChild(itemContent);
list.appendChild(item);
};
`

Copy link
Author

@jiwonnchoi jiwonnchoi Sep 9, 2024

Choose a reason for hiding this comment

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

감사합니다. 수정코드 방향으로 고쳐보며 다시 한번 이벤트 위임의 개념을 다져봐야 할 것 같습니다🥹 디테일하게 리뷰해주셔서 더 많이 고민해보는 계기도 되었고 정말 많이 배워갑니다🙇‍♀️!!


return btn;
};

// 입력창 초기 숨김
form.style.display = "none";

// 입력창 열고 닫는 함수
const toggleForm = () => {
if (form.style.display === "none") {
form.style.display = "flex";
showMessage.innerHTML = "입력창 다시닫기";
} else {
form.style.display = "none";
showMessage.innerHTML = "입력창 불러오기";
}
// 항목 출력하기 함수
const printItem = (text, type) => {
const list = document.querySelector(`.${type}List`);
const item = document.createElement("li");
const itemContent = document.createElement("div");
const itemText = document.createElement("span");
itemText.innerText = text;
itemText.className = `${type}-text`;

// 체크 버튼 생성하기
const checkBtn = createBtn(
type === "todo" ? "images/empty_checkbox.png" : "images/full_checkbox.png",
"todo-check",
type === "todo" ? todoToDid : didToTodo
);
Copy link
Member

Choose a reason for hiding this comment

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

조건문으로 printItem에서 type에 따라 다른 이미지를 사용하는 구성이 정말 깔끔한 로직인 것 같습니다!

Copy link

Choose a reason for hiding this comment

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

check box 디자인을 위해 사진을 활용하는 방법은 생각해보지 못 했던 것 같습니다. 새로운 배움주셔 감사합니다!


// 삭제 버튼 생성하기
const deleteBtn = createBtn(
"images/delete_btn.png",
"todo-del",
type === "todo" ? deleteTodoItem : deleteDidItem
);

// 항목 구성하기
itemContent.className = "todo-item";
itemContent.appendChild(checkBtn);
itemContent.appendChild(itemText);
itemContent.appendChild(deleteBtn);

item.appendChild(itemContent);
list.appendChild(item);
};

// 할 일 추가 함수
// 할 일 추가하기 함수
const addTodoItem = (event) => {
Copy link
Member

Choose a reason for hiding this comment

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

image

항목의 내용이 동일한 경우 하나만 삭제해도 그 동일 항목은 모두 삭제되는 버그가 있어요! 필터링 기준이 텍스트로만 의존되어서 같은 텍스트를 가진 모든 항목이 배열에서 제거되는 것 같아요.

이러한 리스트나 배열 부분을 추가하거나 삭제할 경우 항목을 구분할 수 있는 고유한 식별자(예: id 또는 timestamp)를 추가하여 각 항목을 고유하게 식별하는 것을 추천드려요!

Suggested change
const addTodoItem = (event) => {
// 할 일 추가하기 함수 (고유 id 추가)
const addTodoItem = (event) => {
event.preventDefault();
const todoInput = document.querySelector(".input").value.trim(); // 입력값 공백 확인
if (todoInput) {
const todoItem = { id: Date.now(), text: todoInput }; // 고유 id 추가
todos.push(todoItem);
saveToLocalStorage("todos", todos); // 업데이트된 todos 배열을 localStorage에 저장
printItem(todoItem, "todo"); // 객체 전체 전달
document.querySelector(".input").value = ""; // 입력창 초기화
updateCounts();
}
};

Copy link
Author

Choose a reason for hiding this comment

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

미처 확인해보지 못한 케이스인데, id로 배열의 요소를 관리하는 것은 어떻게 보면 기본이었다는 생각이 들어 반성합니다ㅜ.ㅜ (pr에도 쓴 아쉬운 점의 현상이 아무래도 동일 항목을 입력했을 때였을 거라고 예상됩니다.) 알려주신 부분 반영해보겠습니다! 감사합니다.

event.preventDefault();
const todoInput = document.querySelector(".input").value;
const todoInput = document.querySelector(".input").value; // 입력값
if (todoInput) {
todos.push(todoInput);
saveToLocalStorage("todos", todos); // 저장
printTodo(todoInput);
saveToLocalStorage("todos", todos); // 업데이트된 todos 배열을 localStorage에 저장
printItem(todoInput, "todo");
document.querySelector(".input").value = ""; // 입력창 초기화
}
};

// 할 일 화면에 출력 함수
const printTodo = (text) => {
const todoList = document.createElement("li");
const todoItem = document.createElement("div");
const todoCheck = document.createElement("button");
const todoDel = document.createElement("button");

// 입력한 할 일 내용
const todoText = document.createElement("span");
todoText.innerText = text;
todoText.className = "todo-text";

// 할 일 체크 버튼
const todoCheckImg = document.createElement("img");
todoCheckImg.setAttribute("src", "images/empty_checkbox.png");
todoCheck.appendChild(todoCheckImg);
todoCheck.setAttribute("class", "todo-check");
todoCheck.addEventListener("click", todoToDid);

// 할 일 삭제 버튼
const todoDelImg = document.createElement("img");
todoDelImg.setAttribute("src", "images/delete_btn.png");
todoDel.appendChild(todoDelImg);
todoDel.setAttribute("class", "todo-del");
todoDel.addEventListener("click", deleteTodoItem);

todoItem.className = "todo-item";
todoItem.appendChild(todoCheck);
todoItem.appendChild(todoText);
todoItem.appendChild(todoDel);

todoList.appendChild(todoItem);

document.querySelector(".todoList").appendChild(todoList);
document.querySelector(".input").value = "";
// 항목 삭제하기 함수
const deleteItem = (e, classSelector, array, key, listSelector) => {
Copy link
Member

Choose a reason for hiding this comment

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

삭제하는 부분도 위에서 언급했듯이 id 식별자를 통해서 삭제하는 방식으로 바꾸면 버그를 없앨 수 있을 것 같아요

const target = e.target.closest("li");
const text = target.querySelector(classSelector).innerText;
array = array.filter((item) => item !== text);
saveToLocalStorage(key, array);
document.querySelector(listSelector).removeChild(target);
return array;
};

// 할 일 삭제 함수
const deleteTodoItem = (e) => {
const target = e.target.parentNode.parentNode.parentNode;
const text = target.querySelector(".todo-text").innerText;
todos = todos.filter((todo) => todo !== text);
saveToLocalStorage("todos", todos); // 업데이트
document.querySelector(".todoList").removeChild(target);
todos = deleteItem(e, ".todo-text", todos, "todos", ".todoList");
};

const deleteDidItem = (e) => {
dones = deleteItem(e, ".did-text", dones, "dones", ".didList");
};

// 할 일에서 한 일로 옮기기
// 할 일에서 한 일로 이동 함수
const todoToDid = (e) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

함수 네이밍을 할 때, 함수 이름만으로 동작을 예측할 수 있도록 명확한 역할을 표현하는 이름을 사용하는 것이 좋다고 해요!
deleteTodoItem은 할일을 삭제한다는 의미가 명확하게 드러나는 함수명인 반면,
todoToDid, didToTodo는 to가 작업의 방향성을 나타내긴 하지만, 할 일 항목을 완료 상태로 변경, 완료된 항목을 다시 미완료 상태로 변경한다는 의미를 한번에 전달하기에는 약간 광범위한 표현이라는 생각이 들어요! (todo가 정확히 미완료 할일만을 가리키는거라기보단 할일 목록을 가리킬 수도 있어 표현이 약간 광범위해보임 )
때문에, todoToDid보다는 함수의 역할이 명확하게 드러나도록 completeTodo
didToTodo 보다는 restoreTodo 처럼 함수명을 수정해보는 것을 제안드려봅니다!

https://www.youtube.com/watch?v=emGLxi0LvNI&t=556s
명확한 네이밍을 통해 코드 가독성을 개선하는 것에 대한 카카오 FE개발팀 발표 영상 첨부해드려요!!
어떻게 변수명/함수명 등을 지어야 할지 기준을 세우는데 도움을 줄 수 있는 좋은 영상인 것 같아서 첨부합니다~~
11분 18초 '2. 보다 구체적인 단어로 바꾸기' 섹션부터 참고하시면 좋을 것 같아요!(앞부분도 유익해서 시간되시면 다 보시는것도 추천드려용ㅎㅎ )

또한, 보통 did는 동작을 가리키는 반면에 done은 상태를 가리키는 표현이기 때문에, did보단 done을 사용하는 것이 적절해보여요!!

Copy link
Author

Choose a reason for hiding this comment

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

네이밍은 매번 고민하던 부분이었는데 이번을 계기로 좀더 명확한 기준을 확립해보겠습니다! 영상 추천해주셔서 감사합니다 ☺️

const target = e.target.parentNode.parentNode.parentNode;
const target = e.target.closest("li");
const todoText = target.querySelector(".todo-text").innerText;
deleteTodoItem(e);
todos = deleteItem(e, ".todo-text", todos, "todos", ".todoList");
dones.push(todoText);
saveToLocalStorage("dones", dones); // 한 일 저장
printDidItem(todoText);
};

// 한 일 출력 함수
const printDidItem = (text) => {
const didList = document.createElement("li");
const didItem = document.createElement("div");
const didCheck = document.createElement("button");
const didDel = document.createElement("button");

const didText = document.createElement("span");
didText.innerText = text;
didText.className = "did-text";

// 한 일 체크 버튼
const didCheckImg = document.createElement("img");
didCheckImg.setAttribute("src", "images/full_checkbox.png");
didCheck.appendChild(didCheckImg);
didCheck.setAttribute("class", "todo-check");
didCheck.addEventListener("click", didToTodo);

// 한 일 삭제 버튼
const didDelImg = document.createElement("img");
didDelImg.setAttribute("src", "images/delete_btn.png");
didDel.appendChild(didDelImg);
didDel.setAttribute("class", "todo-del");
didDel.addEventListener("click", deleteDidItem);

didItem.className = "todo-item";
didItem.appendChild(didCheck);
didItem.appendChild(didText);
didItem.appendChild(didDel);

didList.appendChild(didItem);

document.querySelector(".didList").appendChild(didList);
printItem(todoText, "did");
};

// 한 일 삭제 함수
const deleteDidItem = (e) => {
const target = e.target.parentNode.parentNode.parentNode;
const text = target.querySelector(".did-text").innerText;
dones = dones.filter((done) => done !== text);
saveToLocalStorage("dones", dones); // 업데이트
document.querySelector(".didList").removeChild(target);
};

// 한 일을 다시 할 일로 옮기기
// 한 일에서 할 일로 이동 함수
const didToTodo = (e) => {
const target = e.target.parentNode.parentNode.parentNode;
const target = e.target.closest("li");
const didText = target.querySelector(".did-text").innerText;
deleteDidItem(e);
dones = deleteItem(e, ".did-text", dones, "dones", ".didList");
todos.push(didText);
saveToLocalStorage("todos", todos); // 할 일 저장
printTodo(didText);
printItem(didText, "todo");
};

/* todo 입력, 체크, 삭제 */
const form = document.querySelector(".input-box"); // 입력창 폼 요소
const showMessage = document.querySelector(".show-input"); // 입력창 열고 닫는 버튼 요소

// 입력창 열고 닫기 함수
const toggleForm = () => {
Copy link
Member

Choose a reason for hiding this comment

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

이런식으로 스타일을 통해서 관리하는 것도 좋지만 입력창의 상태(열림/닫힘)를 명확하게 추적하기 위해 boolean 변수로 상태를 관리하여 여닫는것도 유지보수하기엔 더 편하니까 한번 사용해봐도 좋을 것 같아요 :)_

Copy link
Author

Choose a reason for hiding this comment

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

클래스명은 워낙 지정하기 나름이라 혼동할 수도 있고, html/css에서 변경될 가능성도 있어 확실히 클래스 변경에 의존하는 것보다는 말씀해주신 것이 훨씬 직관적이고 혼란을 줄일 수 있겠다는 생각이 듭니다! 좋은 의견 주셔서 감사합니다☺️

form.style.display = form.style.display === "none" ? "flex" : "none";
showMessage.innerHTML =
form.style.display === "none" ? "입력창 불러오기" : "입력창 다시닫기";
};

// 이벤트 리스너 등록 및 기존 데이터 불러오기 함수
const init = () => {
form.addEventListener("submit", addTodoItem);
showMessage.addEventListener("click", toggleForm);
initTodoList();
};

init();
Copy link

Choose a reason for hiding this comment

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

window.onload / document.addEventListener("DOMContentLoaded" / 이벤트 리스너 등록없이 함수 실행

이라는 렌더링 방식에 있어 각각의 사용목적과 장단점이 있음을 이번에 알게 되었습니다. 링크 한번 달겠습니다! 한번 참고하셔도 좋을 것 같습니다. (링크를 줄이는 방법을 몰라 링크가 긴 점 양해부탁드립니다..)

https://mesonia.tistory.com/entry/%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%A1%9C%EB%93%9C-%ED%9B%84-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0-windowonload-documentready-documentready%EB%A5%BC-%EC%88%9C%EC%88%98%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A1%9C-DOMContentLoaded