전반적인 구조

위는 Critical Rendering Path (CRP)로
브라우저가 HTML -> 화면 픽셀로 바꾸는 과정 중
처음 화면을 그리기까지 필요한 핵심 단계를 의미한다. (최소 렌더링 과정)
DOM (Document Object Model): HTML과 다르게 JS로 조작 가능한 객체 형태이다.
이 CRP가 느리면
- First Paint 늦어짐
- UX 나빠짐
- SEO에도 영향이 감
=> Blocking(다 받아야 다음단계로 넘어감)한 CSS
JS로 할 수 있는 것
- 내용변경 (textContent)
- 스타일변경 (style.cssName)
- 요소 추가/ 삭제 (document.createElement() / appendChild)
- 이벤트 처리 (addEventListener)
- 애니메이션 (style.transition / style.transform)
- 데이터 통신 (fetch('url').then())
웹 브라우저가 HTML, CSS, JS를 파싱하고 읽어서 DOM을 생성/조작을 한다.


DOM 선택하기
- document.querySelector(".className")
- document.querySelector("#idName")
- document.querySelectorAll(".className")
ㄴ NodeList를 리턴함, snapshot만 가져
- document.getElementById("idName")
- document.getElementsByClassName("className");
이러면 여러개 선택되는데, 유사배열 패턴이라 배열함수를 사용하려면 배열로 바꿔야한다.
유사배열 -> 배열하는 가장 간단한 방법은 spread operator을 사용하는 것이다.
[...selectedElements].forEach(box => {box.addEventListener()});
이런식으로 순회하면 된다.
- document.getElementsByTagName("tagName") -> 모든 태그 찾기
selectedElement.getElementsByTagName("tagName") -> selectedElement안에서 모든 태그 찾기
ㄴ HTMLCollection을 리턴함 (빠름, 항상 현재 DOM을 반영하는 Live한 형태라 헷갈림)

// 자손 선택자
const subtitle = document.querySelector('#header p');
const intro = document.querySelector('.content .intro');
// 자식 선택자
const directChild = document.querySelector('#header > h1');
// 속성 선택자
const activeItem = document.querySelector('li.active');
const firstLi = document.querySelector('li:first-child');
const lastLi = document.querySelector('li:last-child');
const secondLi = document.querySelector('li:nth-child(2)');
// 여러 클래스
document.querySelector('.box.highlight'); // 두 클래스 모두 있는 요소
// 형제 선택자
document.querySelector('h1 + p'); // h1 바로 다음 p
document.querySelector('h1 ~ p'); // h1 다음의 모든 p
// nth-child (pseudo class - 가상 선택자)
document.querySelector('li:nth-child(2)'); // 두 번째 li
document.querySelector('li:nth-child(odd)'); // 홀수 번째
document.querySelector('li:nth-child(even)'); // 짝수 번째
// 속성
document.querySelector('[type="text"]'); // type이 text인 요소
document.querySelector('[data-id="123"]'); // data-id가 123
document.querySelector('input[required]'); // required 속성이 있는 input
// not
document.querySelector('li:not(.active)'); // active 아닌 li
=> 웬만해서는 getElementById, querySelector이랑 querySelectorAll을 사용한다.
- selectedElement.parentElement: 부모 요소
- selectedElement.closest(): (부모중) 가장 가까운 특정 element 찾기
- selectedElement.children
- selectedElement.firstElementChild
- selectedElement.lastElementChild
- selectedElement.previousElementSibling
- selectedElement.nextElementSibling
id/class 수정
selectedElement.id = "newId"
텍스트 수정
selectedElement.textContent = "new text"
selectedElement.innerText도 있으나, CSS 눈치를 봐서 (display:none이면 아무것도 안 나옴) 느려서 실무에서는 안 씀7
안의 HTML 수정
selectedElement.body.innerHTML = "<div></div>" (안쪽의 HTML만 기재)
selectedElement.body.outerHTML은 본인을 포함한다.
HTML 속성 수정
selectedElement.style.color = "red"; -> styleSheet안에 있는건 못 본다
window.getComputedStyle(selectedElement);
가급적이면 inline style을 직접 바꾸지는 않고, CSS에다가 클래스를 정의해서 클래스를 추가했다 빼는 로직이 더 보편적이다
selectedElement.src = "abc.png"
selectedElement.classList.add("")
selectedElement.classList.remove("")
selectedElement.classList.toggle("")
selectedElement.classList.contains("")
selectedElement.classList.replace("old", "new")
selectedElement.setAttribute("data-id", "123"); //커스텀 속성 넣을때 필요함
selectedElement.getAttribute() //커스텀 속성 꺼낼때 필요함
selectedElement.hasAttribute()
요소 추가 / 삭제 / 수정
- const newElement = createElement("div")
- selectedElement.append(newElement, newElement2, newElement3)
이때 newElement~newElement3는 한꺼번에 들어간다
이미 DOM에 있는 요소를 append하면 복사가 아니라 이동됩니다
children의 맨 끝으로 들어간다.
const li1 = document.createElement("li");
li1.textContent = '항목1';
const li2 = document.createElement("li");
li2.textContent = '항목2';
const text = '텍스트도 가능';
const arr = [li1, li2, text];
list.append(...arr);
- selectedElement.prepend(newElement, newElement2...)
children의 맨 앞으로 들어간다.
- selectedElement.before/after(newElement, newElement2, newElement3)
형제로 추가하는 방식이다.
- selectedElement.insertAdjacentHTML("option", newElement)

- 추가 외의 삭제/수정 메서드
- selectedElement.remove()
자신을 삭제하는 메서드이다.
- selectedElement.removeChild(childElement)
자식을 삭제하는 메서드이다.
//모든 child 지우기
while (continer.firstChild){
container.removeChild(container.firstElementChild);
}
⭐단순히 selectedElement.innerHTML = ''하는거는 eventListener을 지우지 않기 때문에 권장하지 않는다.
- selectedElement.replaceChildren(newElements)
자식을 대체하는 메서드 (안의 자식들이 안전하게 싹다 지워지고, 새 내용으로 대체됨)
- selectedElement.replaceChild(newChild, oldChild)
자식요소 하나를 대체하는 메서드
비표준 속성 다루기 (data-*)
- data-로 시작함
<div
id="user"
data-id="123"
>
사용자 정보
</div>
- 사용자 정의 데이터 저장
- HTML 표준 (CSS도 사용 가능)
const user = document.querySelector('#user');
// 읽기 (카멜 케이스로 변환됨)
console.log(user.dataset.id); // "123"
console.log(user.dataset.name); // "홍길동"
console.log(user.dataset.role); // "admin"
console.log(user.dataset.createdAt); // "2024-01-20"
// 쓰기
user.dataset.status = 'active';
user.dataset.lastLogin = new Date().toISOString();
// 삭제
delete user.dataset.status;
// 전체 data 객체
console.log(user.dataset);
.status[data-status="success"] {
color: green;
}
✅ 사용처
이벤트가 발생한 element가 정확히 무엇인지 파악하기 위해서 사용한다.
ex) 클릭한 탭에 따른 내용 보여주기
❄️예제
function test6() {
console.log("\n=== 테스트 6: 탭 메뉴 활성화 ===");
const tabs = document.querySelectorAll(".tab");
const contents = document.querySelectorAll(".tab-content");
// 이벤트 리스너는 제공됩니다 (아래 TODO만 채워주세요)
tabs.forEach((tab) => {
tab.addEventListener("click", (e) => {
// TODO: tab의 dataset에서 tab 값을 읽어 targetTab 변수에 저장하세요
const targetTab = tab.dataset.tab;
// TODO: 모든 탭 버튼에서 "active" 클래스를 제거하세요 (tabs를 forEach로 순회)
tabs.forEach((tab) => {
tab.classList.remove("active");
});
// TODO: 모든 탭 콘텐츠에서 "active" 클래스를 제거하세요 (contents를 forEach로 순회)
contents.forEach((active) => {
active.classList.remove("active");
});
// TODO: 클릭한 tab에 "active" 클래스를 추가하세요
e.target.classList.add("active");
// TODO: data-content 값이 targetTab과 일치하는 콘텐츠에 "active" 클래스를 추가하세요
document
.querySelector(`[data-content="${targetTab}"]`)
.classList.add("active");
// hint: document.querySelector(`[data-content="${targetTab}"]`)
console.log("활성 탭:", targetTab);
});
});
console.log("탭 메뉴 이벤트 등록 완료! 위의 탭 버튼을 클릭해보세요.");
}
이벤트 리스너 등록
이벤트: 웹 페이지에서 발생하는 모든 사
- selectedElement.addEventListener("eventName", 헨들러함수)

한번만 실행하려면 다음 옵션을 추가하면 된다.
button.addEventListener('click', function() {
console.log('한 번만 실행');
}, { once: true });
selectedElement.onclick = () => {}는 덮어쓰기 방식이라서 여러개 정의하면 마지막 것이 등록된다.
반면 addEventListener은 추가하는 방식이라서 여러개 정의하면 모두 발현된다.
메모리 (RAM)에 이벤트 리스너를 등록하기 때문에 너무 많이 등록하면 컴퓨터가 힘들어진다.
그래서 쓰지 않는 이벤트 리스너는 삭제해줘야한다.
- selectedElement.removeEventListener("eventName", 헨들러함수)
익명 함수는 제거 불가능하다
(+) 이벤트 객체: 이벤트 발생 시 전달되는 객체이다. 보통 e라고 표현한다.

e.target (이벤트가 일어난 요소)와 e.currentTarget (이벤트 리스너가 등록된 요소) 정도만 알면 된다.
e.clientX, e.clientY도 알면 좋다 (브라우저 창 크기 / 뷰포트 기준으로 - url창, 탭들 포함 X)
e.screenX, e.screenY (모니터 기준으로)
e.key하면 keyboard 이벤트일 경우 무슨 키를 눌렀는지 보여준다.
e.preventDefault()도 많이 쓰인다. 기본 동작을 막아준다. (a 태그의 링크 이동, form 태그의 폼 제출 방지, 우클릭/복사 방지)
⭐ 이벤트 전파
하나의 이벤트가 DOM 트리를 따라 위아래로 흘러가는 현상이다.
브라우저가 이벤트를 인식하는 방법과 관련있다.
클릭 한 번 했을 뿐인데, 그 이벤트는 3단계를 거칩니다:
1단계: 캡처링 (위 → 아래) - window에서 출발해서 클릭된 요소까지 내려감
2단계: 타깃 - 실제 클릭된 요소에 도착
3단계: 버블링 (아래 → 위) - 클릭된 요소에서 다시 window까지 올라감

브라우저는 내가 어디 클릭했는지 다 알고 있는데,
그 정보들을 모두 이벤트 전파를 통해서 파악한다.
캡처링은 몰라도, 버블링이 일어나고 있다는 것은 잘 알고 있어야한다.
버튼을 클릭했는데 뒤에 있는 div의 이벤트까지 같이 실행되는 경험을 해봤을것이다. 그게 바로 버블링 때문에 일어나는 것이다.
버블링은 target에서 window로 다시 나가는 방향으로 흐르기 때문에
밑의 예제에서 button > innder > outer 순으로 출력된다
<div id="outer" style="padding: 30px; background: #ddd;">
<div id="inner" style="padding: 30px; background: #aaa;">
<button id="btn">클릭!</button>
</div>
</div>
<script>
document.getElementById('outer').addEventListener('click', () => {
console.log('outer 클릭');
});
document.getElementById('inner').addEventListener('click', () => {
console.log('inner 클릭');
});
document.getElementById('btn').addEventListener('click', () => {
console.log('button 클릭');
});
// 버튼 클릭 시 출력 순서:
// "button 클릭" ← 타깃
// "inner 클릭" ← 버블링
// "outer 클릭" ← 버블링
</script>
ps. capturing 단계에서 eventListener이 발동했으면 좋겠으면,
.addEventListner(이벤트, 함수, true)하면 된다. 하지만 쓰이는 경우가 별로 없다.
e.stopPropagation()
button만 이벤트 확인하고, window로 돌아가는 길에 애들은 확인하지 않기 때문에
드디어 button 이벤트만 발생하고, button의 부모 이벤트들은 발현하지 않는다.
모달 구현할때 사용 할 수는 있긴 한데 보통은 아래아래의 이미지와 같게 작성한다.
document.getElementById('btn').addEventListener('click', (e) => {
e.stopPropagation(); // 여기서 전파 중단!
console.log('button 클릭');
});
document.getElementById('outer').addEventListener('click', () => {
console.log('outer 클릭'); // 실행 안 됨!
});
// 버튼 클릭 시 출력:
// "button 클릭" ← 이것만 실행됨

웬만하면 stoppropagation은 자주 안 쓰는 것이 좋긴 하다. (보통 버블링 막지 않고 그냥 흐르게 둠)
왜냐하면 나중에 관리 툴을 사용해서 사용자 사용 패턴 같은 것을 파악할건데
그런 툴들은 버블링을 사용하기 때문이다.
이벤트 위임
버블링을 활용해서, 이벤트 대상한테 이벤트 리스너를 등록하는게 아닌,
버블링 경로 (부모)에게 이벤트 리스너를 대신 적용하는 방식이다.
실무에서 정말 많이 사용하는 패턴이다.
항목마다 이벤트 등록을 하는 것이 아니라, 부모에 한번만 등록할 수 있게 한다.
코드도 간결해지고, 이벤트 리스너를 덜 등록할 수 있게 해주기 때문에 좋다.
// ❌ 비효율: 항목마다 이벤트 등록
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', () => {
console.log(li.textContent);
});
});
// ✅ 효율: 부모에 한 번만 등록 (이벤트 위임)
document.querySelector('ul').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log(e.target.textContent);
}
});
<ul id="todo-list">
<li>공부하기 <button class="delete">삭제</button></li>
<li>운동하기 <button class="delete">삭제</button></li>
</ul>
<button id="add">할 일 추가</button>
<script>
const list = document.getElementById('todo-list');
// 이벤트 위임: ul에 한 번만 등록
list.addEventListener('click', (e) => {
if (e.target.classList.contains('delete')) {
e.target.closest('li').remove();
}
});
// 새 항목 추가 — 이벤트 등록 없이도 삭제 버튼이 바로 동작!
document.getElementById('add').addEventListener('click', () => {
const li = document.createElement('li');
li.innerHTML = '새 할 일 <button class="delete">삭제</button>';
list.appendChild(li);
});
</script>
XSS 공격 대비
JS 문법은 무조건 js파일에 작성하거나,
html에다가 작성할꺼면 <script>로 감싸줘야한다.
그래야 XSS 공격에 안전하다.
동기 vs 비동기 실행
동기: synchronous, 순차적으로 실행됨. 다음 작업은 앞의 작업이 끝날때까지 기다린다.
비동기: asynchronous, 동시에 여러 작업
비동기가 필요한 경우:
- 오래 걸리는 작업
동기적으로 처리하면 블로킹 현상이 일어남
ex) 서버에서 데이터 가져오기 (fetch) / 타이머 / 사용자 입력 대기 (eventListner) / 파일 읽기(readFile)
JS 실행기 nodeJS는 싱글 스레드라서 직접 여러 작업을 동시에 실행할 수 없다.
JS는 싱글 스레드 언어라서 한 번에 하나의 일만 할 수 있다.
비동기를 가능하게 해주는 것이 바로 브라우저가 제공하는 event loop 시스템이다.

JS 엔진 (V8)
JS 코드를 읽고 실행하는 엔진디ㅏ.
브라우저 안에 내장되어 있으며, 크게 두가지 메모리로 구정되어 있습니다.
Call Stack: "지금 당장 해야하는 일"
- JS가 싱글 스레드인 이유 - 한번에 하나의 일만 처리 가능 (싱글 스레드)
- 현재 실행중인 코드를 쌓아두는 곳
- 실행되려면 무조건 call stack을 거쳐야함 (그것이 callback이어도)
- 스택에 쌓인 일들을 위에서부터 하나씩 순서대로 처리
ex) main()→ logger("a") → console.log("a")순으로 차곡차곡 쌓이고, 위에서부터 하나씩 실행 후 빠집니다.
Heap: "데이터 보관 창고"
- 변수, 객체 등 데이터가 저장되는 메모리 공간입니다.
- 콜 스택이 "실행"을 담당한다면, 힙은 "저장"을 담당합니다.
Web APIs: 브라우저가 제공하는 외부 기능
- 역할: JS 엔진이 직접 처리하기엔 시간이 오래 걸리는 일을 브라우저가 대신 처리해주는 기능입니다.
- 핵심: Web API는 JS 엔진의 기능이 아니라, 브라우저의 내장 기능입니다.
- 주요 기능:
- setTimeout / setInterval: 지정한 시간 동안 기다리기 (타이머)
- addEventListner, onclick 등: 클릭, 스크롤 같은 이벤트를 감지하고 기다리기
- fetch: 서버에서 데이터를 가져오기 (네트워크 요청)
- 흐름: 콜 스택에서 "이것 좀 처리해줘"라고 Web API에 위임하면, 브라우저가 따로 작업을 처리합니다. 덕분에 콜 스택은 멈추지 않고 다음 일을 할 수 있습니다.
console.log("1번"); // ← Call Stack에서 바로 실행
setTimeout(() => { // ← Call Stack에서 Web API에 타이머 등록만 하고 바로 빠짐
console.log("2번"); // → Web API 타이머 끝나면 콜백이 Callback Queue로 이동
}, 1000);
console.log("3번"); // ← Call Stack에서 바로 실행
콜백 함수
- 다른 함수에 인자로 전달되는 함수
- 비동기 작업의 결과를 처리하는 방법
- callback함수는 본인의 제어권을 자신을 호출하는 함수에게 전달한다.
"니 작업이 끝나면 니가 알아서 나를 호출해줘"
- 웬만하면 화살표 함수로 표현하자 (익명 함수도 가능하긴 함)
- 동기적 콜백, 비동기적 콜백 모두 존재한다.
ex) fetch, addEventListener, setTimeout
Promise가 없던 시절에는
callback queue에 들어가는 비동기 함수들을 순서대로 실행시키려면 콜백을 사용할 수 밖에 없었다
function task1(callback) {
setTimeout(() => {
console.log('작업 1 완료');
callback(); // 다음 작업 실행
}, 1000);
}
function task2(callback) {
setTimeout(() => {
console.log('작업 2 완료');
callback();
}, 1000);
}
function task3() {
setTimeout(() => {
console.log('작업 3 완료');
}, 1000);
}
// 순서대로 실행
task1(() => {
task2(() => {
task3();
});
});
// 출력 (각 1초 간격):
// 작업 1 완료
// 작업 2 완료
// 작업 3 완료
이러면 콜백 지옥(Callback Hell)이 일어날 수 밖에 없다.

-> 가독성 떨어짐, 에러 처리 복잡함, 수정하기 어려움, 디버깅 힘
이는 Promise-Then, 또는 더 최신 문법인 async await를 사용해서 해결한다.
// ✅ Promise
doSomething()
.then(result1 => doSomethingElse(result1))
.then(result2 => doThirdThing(result2))
.then(result3 => console.log(result3));
// ✅ async/await
async function run() {
const result1 = await doSomething();
const result2 = await doSomethingElse(result1);
const result3 = await doThirdThing(result2);
console.log(result3);
}'코드잇 스터디' 카테고리의 다른 글
| [React] 리엑트에 대하여 (0) | 2026.04.21 |
|---|---|
| [JS 기초문법] 리퀘스트 보내기 (0) | 2026.04.14 |
| [JS기초문법] JS 복습해야할 헷갈리는 문법 (0) | 2026.04.06 |
| [JS기초문법] for...of, for...in (0) | 2026.04.06 |
| [Git] Git에 대해서 (0) | 2026.04.01 |