📢 자바스크립트로 오른쪽에서 왼쪽으로 흐르는 문자 만들기(JavaScript Right-to-Left Marquee Text)

📢 자바스크립트로 오른쪽에서 왼쪽으로 흐르는 문자 만들기

뉴스 속보나 광고 배너처럼, 웹페이지 상단 또는 중간에 문자가 오른쪽에서 왼쪽으로 자연스럽게 흐르는 효과를 구현하고 싶을 때가 있습니다. 자바스크립트와 CSS를 조합하면 매우 간단하게 만들 수 있습니다.

✅ 결과 미리보기

📢 지금 등록하면 50% 할인 혜택을 드립니다! 놓치지 마세요! 🎉

🧾 전체 코드 보기

<div class="marquee-wrapper">
  <div class="marquee-text" id="marqueeText">
    📢 지금 등록하면 50% 할인 혜택을 드립니다! 놓치지 마세요! 🎉
  </div>
</div>

<style>
.marquee-wrapper {
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  background-color: #222;
  color: #fff;
  padding: 10px 0;
  border: 2px solid #333;
  position: relative;
}

.marquee-text {
  display: inline-block;
  padding-left: 100%;
  animation: scroll-left 10s linear infinite;
  font-size: 18px;
}

@keyframes scroll-left {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(-100%);
  }
}
</style>

<script>
const messages = [
  '📢 지금 등록하면 50% 할인 혜택!',
  '🚀 새로운 기능이 출시되었습니다!',
  '🎁 신규 회원 이벤트에 참여하세요!',
  '📞 24시간 고객센터 운영 중입니다!'
];

let index = 0;
const marquee = document.getElementById('marqueeText');

setInterval(() => {
  index = (index + 1) % messages.length;
  marquee.textContent = messages[index];
}, 10000);
</script>

📌 마무리

해당 코드는 블로그 상단 공지, 배너, 뉴스 속보, 알림창 등 다양하게 활용될 수 있으며, 자바스크립트를 통해 여러 문장을 순차적으로 출력할 수도 있어 실용성이 높습니다.

자바스크립트 테이블 행/셀 동적 추가 예제(Dynamic Table Row/Cell Addition Example with JavaScript)

자바스크립트로 HTML 동적 테이블 행 추가/삭제 강좌 (VSCode 스타일)

아래는 버튼 클릭으로 테이블 행과 셀을 동적으로 추가 및 삭제하는 실전 예제입니다.
순수 자바스크립트만으로 구현했으며 소스 설명을 단계별로 안내합니다.


1단계: HTML 테이블 및 '행 추가' 버튼 만들기

테이블(<table>)과 '행 추가' 버튼의 기본 구조입니다.
<thead>에는 제목 행, <tbody>에는 데이터 행이 동적으로 들어갑니다.

<table id="myTable">
  <thead>
    <tr>
      <th>이름</th>
      <th>나이</th>
      <th>삭제</th>
    </tr>
  </thead>
  <tbody>
    <!-- 동적으로 행이 생성됨 -->
  </tbody>
</table>

<button id="addRowBtn">행 추가</button>

2단계: CSS로 테이블과 버튼 스타일링 (VSCode 분위기 UP!)

간결하고 보기 좋은 VSCode 풍 CSS를 적용합니다.

table { border-collapse: collapse; width: 60%; margin-bottom: 20px; }
th, td { border: 1px solid #444; padding: 8px; text-align: center; }
.add-btn { background: #2980b9; color: #fff; border: none; border-radius: 3px; padding: 6px 14px; cursor: pointer; }
.del-btn { background: #e74c3c; color: #fff; border: none; border-radius: 3px; padding: 4px 10px; cursor: pointer; }

3단계: 자바스크립트로 행과 삭제 버튼 동적으로 추가/삭제

아래 코드는 '행 추가' 버튼 클릭 시
- 이름, 나이 입력란과 '삭제' 버튼이 포함된 행을 동적으로 생성
- '삭제' 버튼 클릭 시 해당 행만 삭제합니다.

document.addEventListener('DOMContentLoaded', function() {
  var addRowBtn = document.getElementById('addRowBtn');
  var tableBody = document.getElementById('myTable').getElementsByTagName('tbody')[0];

  addRowBtn.addEventListener('click', function() {
    var newRow = document.createElement('tr');

    // 이름 입력 셀
    var tdName = document.createElement('td');
    var inputName = document.createElement('input');
    inputName.type = 'text';
    inputName.placeholder = '이름 입력';
    inputName.style.width = '95%';
    tdName.appendChild(inputName);
    newRow.appendChild(tdName);

    // 나이 입력 셀
    var tdAge = document.createElement('td');
    var inputAge = document.createElement('input');
    inputAge.type = 'number';
    inputAge.placeholder = '나이';
    inputAge.style.width = '80%';
    inputAge.min = 0;
    inputAge.max = 120;
    tdAge.appendChild(inputAge);
    newRow.appendChild(tdAge);

    // 삭제 버튼 셀
    var tdDelete = document.createElement('td');
    var delBtn = document.createElement('button');
    delBtn.textContent = '삭제';
    delBtn.className = 'del-btn';
    delBtn.addEventListener('click', function() {
      tableBody.removeChild(newRow);
    });
    tdDelete.appendChild(delBtn);
    newRow.appendChild(tdDelete);

    tableBody.appendChild(newRow);
  });
});

4단계: 전체 결과 예시 (동작 미리보기)

아래는 완성된 테이블 UI 미리보기입니다.
코드를 합쳐서 붙여넣으면 블로그에서도 바로 동작합니다.

<style>
table { border-collapse: collapse; width: 60%; margin-bottom: 20px; }
th, td { border: 1px solid #444; padding: 8px; text-align: center; }
.add-btn { background: #2980b9; color: #fff; border: none; border-radius: 3px; padding: 6px 14px; cursor: pointer; }
.del-btn { background: #e74c3c; color: #fff; border: none; border-radius: 3px; padding: 4px 10px; cursor: pointer; }
</style>

<table id="myTable">
  <thead>
    <tr>
      <th>이름</th>
      <th>나이</th>
      <th>삭제</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

<button class="add-btn" id="addRowBtn">행 추가</button>

<script>
document.addEventListener('DOMContentLoaded', function() {
  var addRowBtn = document.getElementById('addRowBtn');
  var tableBody = document.getElementById('myTable').getElementsByTagName('tbody')[0];

  addRowBtn.addEventListener('click', function() {
    var newRow = document.createElement('tr');

    var tdName = document.createElement('td');
    var inputName = document.createElement('input');
    inputName.type = 'text';
    inputName.placeholder = '이름 입력';
    inputName.style.width = '95%';
    tdName.appendChild(inputName);
    newRow.appendChild(tdName);

    var tdAge = document.createElement('td');
    var inputAge = document.createElement('input');
    inputAge.type = 'number';
    inputAge.placeholder = '나이';
    inputAge.style.width = '80%';
    inputAge.min = 0;
    inputAge.max = 120;
    tdAge.appendChild(inputAge);
    newRow.appendChild(tdAge);

    var tdDelete = document.createElement('td');
    var delBtn = document.createElement('button');
    delBtn.textContent = '삭제';
    delBtn.className = 'del-btn';
    delBtn.addEventListener('click', function() {
      tableBody.removeChild(newRow);
    });
    tdDelete.appendChild(delBtn);
    newRow.appendChild(tdDelete);

    tableBody.appendChild(newRow);
  });
});
</script>

5단계: 실무 팁

  • 각 입력란의 값 검증이나, 행 추가 시 기본값 지정 등도 추가 코딩으로 쉽게 구현할 수 있습니다.
  • 동적 tr 추가 패턴은 ASP.NET, WPF의 DataGrid 컨트롤 동작과 매우 유사합니다.
  • React, Vue, Node.js 등 프레임워크와도 이 구조를 쉽게 연계할 수 있습니다.

자바스크립트 then(), catch(), finally() 쉽게 배우기

자바스크립트 then(), catch(), finally() 쉽게 배우기

자바스크립트 Promise 쉽게 배우기: then(), catch(), finally()

✔ Promise란?

자바스크립트에서 비동기 작업을 처리할 때 사용하는 객체입니다. 비동기란 쉽게 말해 "지금 바로 결과가 나오는 게 아니라 나중에 결과가 나오는 작업"을 말합니다.

✔ Promise 기본 구조

const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("성공!");
  } else {
    reject("실패!");
  }
});

✔ then() - 성공했을 때 실행

promise.then(result => {
  console.log("성공 결과:", result);
});

✔ catch() - 실패했을 때 실행

promise.catch(error => {
  console.error("에러 발생:", error);
});

✔ finally() - 성공/실패 상관없이 실행

promise.finally(() => {
  console.log("작업 완료!");
});

✔ 실전 예제 - 버튼 클릭 시 Promise 실행

버튼을 눌렀을 때 1초 후 성공/실패를 랜덤하게 보여주는 예제입니다.

<button onclick="runAsync()">비동기 작업 실행</button>

<script>
function runAsync() {
  new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;
    setTimeout(() => {
      if (success) resolve("서버 응답 성공!");
      else reject("서버 에러 발생!");
    }, 1000);
  })
  .then(msg => alert("✅ " + msg))
  .catch(err => alert("❌ " + err))
  .finally(() => console.log("작업 완료"));
}
</script>

📌 요약

  • then() → 성공했을 때 실행
  • catch() → 실패했을 때 실행
  • finally() → 성공/실패 상관없이 항상 실행

JavaScript 이벤트 버블링(Bubbling)과 캡처링(Capturing) 완벽 이해: 이벤트 흐름 제어 마스터하기(JavaScript Event Bubbling and Capturing Explained: Mastering Event Flow Control)

이벤트 버블링(Bubbling)과 캡처링(Capturing) 완벽 이해: 이벤트 흐름 제어 마스터하기

웹 페이지에서 사용자가 버튼을 클릭하거나, 마우스를 올리거나, 키보드를 누르는 등 다양한 '이벤트(Event)'가 발생합니다. JavaScript는 이런 이벤트를 감지하고, 그에 따라 특정 동작을 실행하게 만듭니다. 우리는 지난 포스팅에서 addEventListener()를 통해 이벤트를 다루는 방법을 간략히 살펴보았죠.

그런데 웹 페이지의 요소들은 마치 러시아 인형처럼 서로를 감싸는 구조를 가지고 있습니다. 예를 들어, 웹 페이지(`document`) 안에 `<body>`가 있고, `<body>` 안에 `<div>`가, 그 안에 `<button>`이 있는 식이죠. 이때 `<button>`을 클릭하면, 이 클릭 이벤트가 `<button>`에서만 발생하는 것이 아니라, 그 이벤트를 감싸고 있는 부모 요소들에게도 전달되는 흥미로운 현상이 일어납니다.

이것이 바로 '이벤트 흐름(Event Flow)'의 핵심인 버블링(Bubbling)과 캡처링(Capturing)입니다. 이 두 개념을 이해하면 복잡한 이벤트 처리 로직을 훨씬 깔끔하고 효율적으로 작성할 수 있게 됩니다. 초보자도 쉽게 이해할 수 있도록 그림과 코드를 통해 자세히 알아보겠습니다!

🌊 이벤트 흐름(Event Flow)의 3단계

DOM 이벤트는 클릭이나 키 입력 같은 특정 사건이 발생했을 때, 마치 물결처럼 웹 페이지의 요소들을 따라 전달됩니다. 이 과정은 크게 세 단계로 나뉩니다.

  1. 캡처링(Capturing) 단계: 이벤트가 위에서 아래로 (최상위 부모 요소부터 클릭된 요소까지) 내려가는 단계입니다.
  2. 타겟(Target) 단계: 이벤트가 실제로 발생한 타겟 요소에 도달하는 단계입니다.
  3. 버블링(Bubbling) 단계: 이벤트가 아래에서 위로 (클릭된 요소부터 최상위 부모 요소까지) 다시 올라가는 단계입니다.

대부분의 이벤트는 캡처링 단계에서 시작하여 타겟 단계에 도달한 후, 버블링 단계로 돌아갑니다. 아래 그림을 상상해 보세요.

이벤트 흐름 다이어그램

(위 그림은 이벤트 흐름의 캡처링, 타겟, 버블링 단계를 시각적으로 보여줍니다.)

우리는 addEventListener()를 사용할 때 이 이벤트 흐름 중 어떤 단계에서 이벤트를 감지할지 설정할 수 있습니다. addEventListener(event, handler, options)에서 세 번째 인자 options(또는 useCapture)가 바로 그것입니다.

⬆️ 이벤트 버블링(Event Bubbling): 기본 동작 방식

이벤트 버블링은 대부분의 이벤트에서 기본적으로 동작하는 방식입니다. 특정 요소에서 이벤트(예: 클릭)가 발생하면, 그 이벤트는 마치 물속의 거품이 위로 떠오르듯이 자신을 감싸고 있는 부모 요소들에게 순차적으로 전달(버블링)됩니다.

예를 들어, `<div id="outer">` 안에 `<div id="inner">`가 있고, 이 `inner`를 클릭하면, 클릭 이벤트는 다음과 같은 순서로 전달됩니다.

  1. `<div id="inner">` (이벤트가 실제로 발생한 타겟 요소)
  2. `<div id="outer">` (inner의 부모)
  3. `<body>` (outer의 부모)
  4. `<html>` (body의 부모)
  5. `document` (HTML 문서 전체)
  6. `window` (브라우저 창)

각각의 요소에 이벤트 리스너가 등록되어 있다면, 이 이벤트는 모든 리스너를 트리거하게 됩니다.

버블링 예시 코드

아래 HTML 구조와 JavaScript 코드를 실행해 보고, 어떤 순서로 메시지가 뜨는지 확인해 보세요.


<!-- HTML -->
<div id="outer" class="outer-box">
    Outer Div
    <div id="middle" class="middle-box">
        Middle Div
        <div id="inner" class="inner-box">
            Inner Div (클릭!)
        </div>
    </div>
</div>
  

// JavaScript
document.addEventListener('DOMContentLoaded', () => {
    const outerDiv = document.getElementById('outer');
    const middleDiv = document.getElementById('middle');
    const innerDiv = document.getElementById('inner');

    // 각 요소에 클릭 이벤트 리스너를 추가합니다.
    // 세 번째 인자(options)를 생략하거나 false로 두면 버블링 단계에서 동작합니다.
    outerDiv.addEventListener('click', () => {
        alert('Outer Div 클릭 이벤트 발생 (버블링)');
    });

    middleDiv.addEventListener('click', () => {
        alert('Middle Div 클릭 이벤트 발생 (버블링)');
    });

    innerDiv.addEventListener('click', () => {
        alert('Inner Div 클릭 이벤트 발생 (버블링)');
    });

    // Inner Div를 클릭하면, 'Inner -> Middle -> Outer' 순서로 alert이 뜹니다.
});
  

Inner Div (클릭!)을 클릭하면, 'Inner Div 클릭 이벤트 발생' 알림이 먼저 뜨고, 이어서 'Middle Div 클릭 이벤트 발생', 마지막으로 'Outer Div 클릭 이벤트 발생' 알림이 뜰 것입니다. 이것이 바로 버블링의 증거입니다!

⬇️ 이벤트 캡처링(Event Capturing): 특수한 동작 방식

이벤트 캡처링은 버블링과 반대 방향으로 이벤트가 전달되는 방식입니다. 이벤트가 발생하면, 최상위 부모 요소(예: window 또는 document)부터 시작하여 이벤트가 발생한 실제 타겟 요소까지 아래로 내려오면서 전달됩니다.

이벤트 캡처링은 기본적으로는 잘 사용되지 않지만, 특정 상황에서 유용하게 쓰일 수 있습니다. 예를 들어, 특정 영역 내의 모든 클릭 이벤트를 가장 먼저 감지하여 처리하고 싶을 때 사용할 수 있습니다.

캡처링 예시 코드

addEventListener()의 세 번째 인자에 { capture: true } (또는 예전 방식인 true)를 설정하면 캡처링 단계에서 이벤트를 감지합니다.


// JavaScript
document.addEventListener('DOMContentLoaded', () => {
    const outerDiv = document.getElementById('outer');
    const middleDiv = document.getElementById('middle');
    const innerDiv = document.getElementById('inner');

    // 각 요소에 클릭 이벤트 리스너를 추가합니다.
    // 세 번째 인자를 { capture: true }로 설정하여 캡처링 단계에서 동작하도록 합니다.
    outerDiv.addEventListener('click', () => {
        alert('Outer Div 클릭 이벤트 발생 (캡처링)');
    }, { capture: true }); // 또는 true

    middleDiv.addEventListener('click', () => {
        alert('Middle Div 클릭 이벤트 발생 (캡처링)');
    }, { capture: true }); // 또는 true

    innerDiv.addEventListener('click', () => {
        alert('Inner Div 클릭 이벤트 발생 (캡처링)');
    }, { capture: true }); // 또는 true

    // Inner Div를 클릭하면, 'Outer -> Middle -> Inner' 순서로 alert이 뜹니다.
});
  

Inner Div (클릭!)을 클릭하면, 이번에는 'Outer Div 클릭 이벤트 발생' 알림이 먼저 뜨고, 이어서 'Middle Div 클릭 이벤트 발생', 마지막으로 'Inner Div 클릭 이벤트 발생' 알림이 뜰 것입니다. 캡처링은 이벤트가 위에서 아래로 내려오는 것을 보여줍니다.

🛑 이벤트 흐름 제어하기: stopPropagation()preventDefault()

이벤트 버블링과 캡처링은 유용하지만, 때로는 이벤트가 원치 않는 곳까지 전달되거나, 브라우저의 기본 동작을 막고 싶을 때가 있습니다. 이때 사용할 수 있는 두 가지 중요한 메서드가 있습니다.

1. event.stopPropagation(): 이벤트 전파 막기

이 메서드는 현재 이벤트가 더 이상 다음 요소로 전달(전파)되는 것을 막습니다. 버블링이든 캡처링이든 상관없이, 이 메서드가 호출된 시점에서 이벤트의 전달은 멈춥니다.

💡 사용 예시: 중첩된 요소에서 특정 버튼만 클릭 이벤트를 처리하고, 부모 요소의 이벤트는 발동시키고 싶지 않을 때.


// JavaScript
document.addEventListener('DOMContentLoaded', () => {
    const outerDiv = document.getElementById('outer');
    const middleDiv = document.getElementById('middle');
    const innerDiv = document.getElementById('inner');

    outerDiv.addEventListener('click', () => {
        alert('Outer Div 클릭 이벤트 발생');
    });

    middleDiv.addEventListener('click', () => {
        alert('Middle Div 클릭 이벤트 발생');
    });

    innerDiv.addEventListener('click', (event) => {
        alert('Inner Div 클릭 이벤트 발생');
        event.stopPropagation(); // ⚠️ 여기서 이벤트 전파를 멈춥니다!
    });

    // Inner Div를 클릭하면, 'Inner Div 클릭 이벤트 발생'만 뜨고,
    // Middle Div와 Outer Div의 이벤트는 발생하지 않습니다.
});
  

2. event.preventDefault(): 브라우저 기본 동작 막기

이 메서드는 특정 이벤트에 대한 브라우저의 기본 동작을 막습니다. 예를 들어, <a> 태그를 클릭하면 페이지 이동이 기본 동작인데, `event.preventDefault()`를 사용하면 페이지 이동을 막을 수 있습니다. 폼(form) 제출 시 페이지 새로고침을 막을 때도 사용합니다.

💡 사용 예시: 링크 클릭 시 페이지 이동을 막고 JavaScript로 특정 동작만 수행하고 싶을 때.


<!-- HTML -->
<a id="myLink" href="https://www.google.com">구글로 이동하기</a>
<form id="myForm">
    <input type="text">
    <button type="submit">제출</button>
</form>
  

// JavaScript
document.addEventListener('DOMContentLoaded', () => {
    const myLink = document.getElementById('myLink');
    const myForm = document.getElementById('myForm');

    myLink.addEventListener('click', (event) => {
        event.preventDefault(); // ⚠️ 링크의 기본 동작(페이지 이동)을 막습니다.
        alert('링크 클릭! 하지만 구글로 이동하지 않습니다.');
    });

    myForm.addEventListener('submit', (event) => {
        event.preventDefault(); // ⚠️ 폼 제출 시 기본 동작(페이지 새로고침)을 막습니다.
        alert('폼이 제출되었지만 페이지가 새로고침되지 않습니다.');
        // 여기에서 Ajax 등으로 실제 데이터 전송 로직을 구현합니다.
    });
});
  

✔️ 결론

이벤트 버블링과 캡처링은 JavaScript 이벤트가 DOM 트리를 통해 어떻게 전달되는지를 이해하는 데 매우 중요한 개념입니다. 대부분의 경우 기본 동작인 버블링으로 충분하지만, 때로는 캡처링을 활용하여 특정 시점에 이벤트를 감지할 필요도 있습니다.

그리고 event.stopPropagation()으로 이벤트의 전파를 멈추고, event.preventDefault()로 브라우저의 기본 동작을 막는 방법을 익히면, 여러분은 웹 페이지의 이벤트를 훨씬 더 세밀하게 제어할 수 있게 됩니다. 이 개념들을 잘 활용하여 사용자에게 더 좋고 예측 가능한 웹 경험을 제공하는 멋진 웹 애플리케이션을 만들어 보세요!

JavaScript DOM 조작 마스터하기: 웹 페이지를 동적으로 제어하는 모든 방법 (초보자 가이드)(JavaScript Mastering DOM Manipulation: A Beginner's Guide to Dynamic Web Pages)

DOM 조작 마스터하기: 웹 페이지를 동적으로 제어하는 모든 방법 (초보자 가이드)

웹사이트에 접속했을 때, 버튼을 클릭하면 내용이 바뀌고, 이미지가 슬라이드 되거나, 새로운 목록이 추가되는 것을 보셨을 거예요. 이런 모든 동적인 변화는 바로 DOM 조작을 통해 이루어집니다. 웹 개발에서 JavaScript를 이용해 웹 페이지를 자유자재로 움직이게 만들려면 DOM 조작은 필수적인 기술입니다.

이번 포스팅에서는 DOM이 무엇인지부터 시작해서, 자바스크립트로 DOM을 선택하고, 내용을 바꾸고, 요소를 추가하거나 삭제하는 등 웹 페이지를 동적으로 제어하는 모든 기본적인 방법을 초보자도 쉽게 이해할 수 있도록 자세히 설명해 드릴게요.

🌳 DOM(Document Object Model)이란 무엇인가요?

DOMDocument Object Model의 약자로, 우리말로는 '문서 객체 모델'이라고 부릅니다. 쉽게 말해, 웹 브라우저가 HTML 문서를 이해하기 쉽도록 나무(Tree) 형태의 구조로 만든 모델이에요. 이 모델 덕분에 JavaScript는 HTML 문서의 모든 요소(태그, 속성, 텍스트 등)에 접근하고, 수정하고, 추가하거나 삭제할 수 있게 됩니다.

HTML 문서의 모든 것은 DOM 트리의 '노드(Node)'가 됩니다. 예를 들어, <body> 태그는 하나의 요소 노드이고, 그 안에 있는 <p> 태그도 요소 노드입니다. <p> 태그 안의 텍스트는 텍스트 노드가 되죠.


<!-- HTML 코드 -->
<div id="container">
    <h1>안녕하세요!</h1>
    <p class="greeting">환영합니다.</p>
</div>
  

위 HTML 코드는 DOM 트리로 다음과 같이 표현됩니다.

  • Document (최상위)
    • html
      • head
      • body
        • div (id="container")
          • h1 ("안녕하세요!" 텍스트 노드 포함)
          • p (class="greeting", "환영합니다." 텍스트 노드 포함)

이렇게 HTML 문서를 객체(Object)들의 모델로 만들어주기 때문에, 우리는 JavaScript를 이용해 이 객체들을 조작해서 웹 페이지를 바꿀 수 있는 것입니다.

🔎 DOM 요소 선택하기

DOM을 조작하려면 먼저 어떤 HTML 요소를 바꿀지 '선택'해야 합니다. JavaScript는 다양한 방법으로 DOM 요소를 선택하는 기능을 제공합니다.

1. ID로 선택하기: getElementById()

HTML 요소에 부여된 고유한 ID를 사용하여 요소를 선택합니다. ID는 웹 페이지에서 하나만 존재해야 합니다.


<!-- HTML -->
<div id="myDiv">안녕하세요!</div>
  

// JavaScript
const myDiv = document.getElementById('myDiv');
console.log(myDiv); // <div id="myDiv">안녕하세요!</div>
  

2. CSS 선택자로 선택하기: querySelector(), querySelectorAll()

CSS에서 요소를 선택하는 방식(태그 이름, 클래스, ID, 속성 등)과 동일하게 CSS 선택자(Selector)를 사용하여 요소를 선택합니다.

  • querySelector(): 주어진 선택자에 해당하는 첫 번째 요소 하나만 반환합니다.
  • querySelectorAll(): 주어진 선택자에 해당하는 모든 요소들을 배열처럼 생긴 목록(NodeList)으로 반환합니다.

<!-- HTML -->
<p class="item">항목 1</p>
<p class="item">항목 2</p>
<span>스팬 태그</span>
  

// JavaScript
const firstItem = document.querySelector('.item'); // 첫 번째 <p class="item">
console.log(firstItem); // <p class="item">항목 1</p>

const allItems = document.querySelectorAll('.item'); // 모든 <p class="item">
console.log(allItems); // NodeList(<p.item>, <p.item>)

const spanTag = document.querySelector('span'); // 첫 번째 <span>
console.log(spanTag); // <span>스팬 태그</span>
  

3. 클래스 이름으로 선택하기: getElementsByClassName()

특정 클래스 이름을 가진 모든 요소를 HTMLCollection 형태로 반환합니다.


const itemsByClass = document.getElementsByClassName('item');
console.log(itemsByClass); // HTMLCollection(p.item, p.item)
  

4. 태그 이름으로 선택하기: getElementsByTagName()

특정 태그 이름을 가진 모든 요소를 HTMLCollection 형태로 반환합니다.


const allParagraphs = document.getElementsByTagName('p');
console.log(allParagraphs); // HTMLCollection(p.item, p.item)
  

💡 팁: querySelector()querySelectorAll()이 가장 강력하고 유연합니다. 대부분의 상황에서 이 두 가지를 주로 사용하게 될 것입니다.

✍️ DOM 요소의 내용 변경하기

요소를 선택했다면, 이제 그 안의 내용을 바꿔볼 수 있습니다.

1. 텍스트 내용 변경: textContent

요소 내부의 텍스트 콘텐츠만 가져오거나 설정합니다. HTML 태그는 무시하고 순수한 텍스트로만 처리합니다.


<!-- HTML -->
<p id="myParagraph">원래 텍스트.</p>
  

// JavaScript
const paragraph = document.getElementById('myParagraph');
paragraph.textContent = '새로운 텍스트로 바뀌었어요!';
console.log(paragraph.textContent); // "새로운 텍스트로 바뀌었어요!"
  

2. HTML 내용 변경: innerHTML

요소 내부의 HTML 콘텐츠를 가져오거나 설정합니다. 새로운 HTML 태그를 포함하여 삽입할 수 있습니다.


<!-- HTML -->
<div id="contentDiv">원래 내용.</div>
  

// JavaScript
const contentDiv = document.getElementById('contentDiv');
contentDiv.innerHTML = '<h3>새로운 제목</h3><p>동적으로 추가된 문단입니다.</p>';
// 이제 contentDiv 안에는 

태그가 생깁니다.

⚠️ 주의: innerHTML은 편리하지만, 사용자에게서 입력받은 값을 그대로 사용하면 XSS(Cross-Site Scripting) 공격에 취약해질 수 있으니 주의해야 합니다.

➕➖ DOM 요소 추가 및 삭제하기

웹 페이지에 새로운 요소를 만들거나, 기존 요소를 없애는 방법입니다.

1. 새 요소 만들기: createElement()

새로운 HTML 태그 요소를 메모리상에 만듭니다. 아직 웹 페이지에는 나타나지 않습니다.


// <li> 태그를 만듭니다.
const newListItem = document.createElement('li');
// <p> 태그를 만듭니다.
const newParagraph = document.createElement('p');
  

2. 텍스트 노드 만들기: createTextNode()

텍스트만을 위한 노드를 만듭니다. 텍스트를 요소에 추가할 때 사용합니다.


const textNode = document.createTextNode('새로운 목록 항목');
// 이 텍스트 노드를 위에서 만든 newListItem에 추가할 수 있습니다.
  

3. 요소에 자식 추가: appendChild()

부모 요소의 가장 마지막 자식으로 새로운 요소를 추가합니다.


<!-- HTML -->
<ul id="myList">
    <li>기존 항목 1</li>
</ul>
  

// JavaScript
const myList = document.getElementById('myList');
const newListItem = document.createElement('li'); // <li> 생성
newListItem.textContent = '새로운 항목 추가!'; // 텍스트 설정

myList.appendChild(newListItem); // myList의 마지막에 newListItem 추가
// 결과: 
  • 기존 항목 1
  • 새로운 항목 추가!

4. 요소 삭제: removeChild()

부모 요소에서 특정 자식 요소를 삭제합니다.


// JavaScript
const myList = document.getElementById('myList');
const firstItem = myList.querySelector('li'); // 첫 번째 <li> 선택

myList.removeChild(firstItem); // myList에서 firstItem 삭제
// 결과: 
    (남아있는 li가 없다면)

    5. 요소 교체: replaceChild()

    부모 요소의 특정 자식 요소를 새로운 요소로 교체합니다.

    
    const parent = document.getElementById('container'); // 부모 요소
    const oldChild = document.querySelector('h1'); // 교체될 기존 자식
    const newChild = document.createElement('h2'); // 새로 들어올 자식
    newChild.textContent = '새로운 제목입니다';
    
    parent.replaceChild(newChild, oldChild); // oldChild를 newChild로 교체
      

    6. 요소 삽입: insertBefore(), prepend()

    • insertBefore(newNode, referenceNode): 특정 참조 노드 앞에 새로운 노드를 삽입합니다.
    • prepend(node): 부모 요소의 가장 첫 번째 자식으로 새로운 노드를 삽입합니다. (appendChild의 반대)
    
    <!-- HTML -->
    <ul id="myList">
        <li id="second">두 번째 항목</li>
        <li>세 번째 항목</li>
    </ul>
      
    
    // JavaScript
    const myList = document.getElementById('myList');
    const secondItem = document.getElementById('second');
    
    // insertBefore 사용 예시: '두 번째 항목' 앞에 '첫 번째 항목' 삽입
    const firstListItem = document.createElement('li');
    firstListItem.textContent = '가운데 삽입된 항목';
    myList.insertBefore(firstListItem, secondItem);
    // 결과: 
    • 가운데 삽입된 항목
    • 두 번째 항목
    • 세 번째 항목
    // prepend 사용 예시: '가장 첫 번째 항목' 삽입 const topListItem = document.createElement('li'); topListItem.textContent = '가장 첫 번째 항목'; myList.prepend(topListItem); // 결과:
    • 가장 첫 번째 항목
    • 가운데 삽입된 항목
    • 두 번째 항목
    • 세 번째 항목

    ⚙️ DOM 요소의 속성과 스타일 변경하기

    HTML 태그의 속성(예: src, href, class, id)이나 CSS 스타일을 변경하는 방법입니다.

    1. 속성 제어: setAttribute(), getAttribute(), removeAttribute()

    • setAttribute(name, value): 요소의 속성 값을 설정하거나, 새로운 속성을 추가합니다.
    • getAttribute(name): 요소의 특정 속성 값을 가져옵니다.
    • removeAttribute(name): 요소의 특정 속성을 삭제합니다.
    
    <!-- HTML -->
    <img id="myImage" src="default.png" alt="기본 이미지">
    <a id="myLink" href="#">링크</a>
      
    
    // JavaScript
    const myImage = document.getElementById('myImage');
    const myLink = document.getElementById('myLink');
    
    // 이미지 src 변경
    myImage.setAttribute('src', 'new_image.png');
    // 이미지 alt 값 가져오기
    console.log(myImage.getAttribute('alt')); // "기본 이미지"
    
    // 링크 href 변경 및 target 속성 추가
    myLink.setAttribute('href', 'https://www.google.com');
    myLink.setAttribute('target', '_blank'); // 새 탭에서 열기
    
    // alt 속성 삭제
    myImage.removeAttribute('alt');
      

    2. 스타일 변경: style 속성 및 classList

    요소의 CSS 스타일을 변경하는 방법입니다.

    • .style 속성: 개별 CSS 속성 직접 변경

      JavaScript에서 CSS 속성 이름을 사용할 때는 하이픈(-) 대신 카멜 케이스(camelCase)를 사용합니다. (예: `background-color` -> `backgroundColor`)

      
      const myDiv = document.getElementById('myDiv');
      myDiv.style.backgroundColor = 'blue';
      myDiv.style.color = 'white';
      myDiv.style.fontSize = '20px';
            
    • .classList 속성: 클래스 추가/삭제 (권장!)

      요소의 클래스 이름을 추가하거나 제거하는 방식으로 스타일을 제어하는 것이 더 효율적이고 유지보수하기 좋습니다. 미리 CSS에 스타일 규칙을 정의해두고 JavaScript로 클래스를 토글하는 방식입니다.

      • add('클래스명'): 클래스 추가
      • remove('클래스명'): 클래스 삭제
      • toggle('클래스명'): 클래스가 있으면 삭제, 없으면 추가 (스위치 기능에 유용)
      • contains('클래스명'): 해당 클래스가 있는지 확인 (true/false 반환)
      
      <!-- HTML -->
      <button id="myButton" class="default-btn">클릭하세요</button>
      <!-- CSS -->
      <style>
        .default-btn { background-color: lightgray; color: black; padding: 10px; }
        .active-btn { background-color: blue; color: white; border: none; }
      </style>
        
      
      // JavaScript
      const myButton = document.getElementById('myButton');
      
      myButton.addEventListener('click', () => {
          // 'active-btn' 클래스를 토글 (있으면 제거, 없으면 추가)
          myButton.classList.toggle('active-btn');
      
          if (myButton.classList.contains('active-btn')) {
              console.log('버튼이 활성화 상태입니다.');
          } else {
              console.log('버튼이 비활성화 상태입니다.');
          }
      });
            

    ⚡ 이벤트 핸들링의 기초: addEventListener()

    사용자가 버튼을 클릭하거나, 마우스를 올리거나, 키보드를 누르는 등 특정 행동을 했을 때 JavaScript 코드를 실행하려면 이벤트 핸들링(Event Handling)이 필요합니다. addEventListener() 메서드를 가장 많이 사용합니다.

    
    // JavaScript
    const myButton = document.getElementById('myButton');
    
    // 'myButton'을 클릭하면 메시지를 띄우는 이벤트 리스너 추가
    myButton.addEventListener('click', () => {
        alert('버튼이 클릭되었습니다!');
    });
    
    // 'myInput'에 키보드를 누를 때마다 콘솔에 입력된 값 출력
    const myInput = document.getElementById('myInput');
    myInput.addEventListener('keyup', (event) => {
        console.log('현재 입력 값:', event.target.value);
    });
      

    💡 DOM 조작 시 성능 팁 (간단히)

    DOM 조작은 웹 페이지 성능에 영향을 줄 수 있습니다. 특히 많은 요소를 자주 조작할 때는 주의해야 합니다.

    • 최소한의 DOM 조작: 여러 개의 요소를 만들거나 수정해야 할 때, 각 요소를 개별적으로 페이지에 추가하기보다는, 먼저 메모리상에서 모든 작업을 완료한 후 한 번에 페이지에 삽입하는 것이 효율적입니다. (예: `DocumentFragment` 사용)
    • innerHTML 남용 주의: 큰 HTML 문자열을 `innerHTML`로 설정하면 기존 내용을 모두 파싱하고 새로 그리는 과정에서 성능 저하가 올 수 있습니다. 꼭 필요한 경우에만 사용하거나, `textContent`를 선호하세요.
    • classList 활용: `style` 속성을 직접 변경하기보다는 CSS 클래스를 추가/제거하는 것이 성능과 유지보수 면에서 더 좋습니다.

    ✨ 결론

    DOM 조작은 웹 페이지를 동적이고 상호작용적인 공간으로 만드는 핵심 기술입니다. HTML 문서를 JavaScript가 이해할 수 있는 트리 형태로 바꾸는 DOM의 개념을 이해하고, 요소를 선택하고, 내용을 바꾸고, 추가하거나 삭제하는 다양한 메서드를 익히는 것은 모든 웹 개발자에게 필수적입니다.

    이 포스팅에서 다룬 기본적인 DOM 조작 방법들을 잘 익히시면, 이제 여러분의 웹 페이지에 생명을 불어넣고 사용자들에게 더욱 풍부한 경험을 제공할 수 있을 것입니다. 지금 바로 직접 코드를 작성하며 DOM 조작의 재미를 느껴보세요!

    자바스크립트 비동기 처리의 악몽, 콜백 헬(Callback Hell) 완벽 해부와 해결 전략 ([JavaScript] Dissecting Callback Hell: Understanding and Conquering Asynchronous Nightmares)

    복잡한 비동기 코드? 콜백 헬(Callback Hell) 완벽 이해와 초보자를 위한 쉬운 해결법

    웹 개발을 하다 보면, 데이터를 가져오거나 무언가를 처리하는 데 시간이 걸리는 경우가 많습니다. 예를 들어, 웹사이트에 접속했을 때 사용자의 정보를 불러오거나, 게시글 목록을 가져오는 작업들이 그렇죠. 이런 작업들을 우리는 '비동기(Asynchronous) 처리'라고 부릅니다.

    그런데 이 비동기 처리를 잘못 사용하면, 코드가 엉망진창이 되고 읽기도, 고치기도 매우 어려워지는 상황을 맞닥뜨릴 수 있습니다. 마치 악몽처럼 복잡해진 코드를 우리는 '콜백 헬(Callback Hell)'이라고 부릅니다. 이번 포스팅에서는 콜백 헬이 무엇인지, 왜 이런 일이 생기는지, 그리고 초보자도 쉽게 따라 할 수 있는 해결 방법들을 자세히 알아보겠습니다.

    🔥 콜백 헬(Callback Hell), 왜 악몽일까요?

    콜백 헬은 말 그대로 '콜백 함수'들이 너무 깊게 중첩되어 코드가 엉망이 되는 현상을 말합니다. 코드가 마치 계단식 피라미드처럼 오른쪽으로 계속 들어가다 보니, 누가 봐도 이해하기 어렵고, 나중에 고치려면 더더욱 힘들어집니다. 그래서 '지옥의 피라미드(Pyramid of Doom)'라고도 불려요.

    콜백 헬은 왜 생길까요?

    JavaScript는 한 번에 하나의 작업만 처리할 수 있는 '단일 스레드(Single Thread)' 언어입니다. 만약 시간이 오래 걸리는 작업(예: 인터넷에서 데이터를 불러오는 작업)을 처리하는 동안 다른 모든 작업이 멈춘다면, 웹사이트가 '멈춘' 것처럼 보일 거예요.

    이런 문제를 해결하기 위해 JavaScript는 '비동기' 방식을 사용합니다. 즉, 시간이 걸리는 작업은 일단 옆으로 미뤄두고 다른 작업을 먼저 처리합니다. 그리고 미뤄뒀던 작업이 끝나면, 미리 약속해둔 함수를 호출해 주는데, 이 약속된 함수가 바로 '콜백 함수(Callback Function)'입니다.

    문제는 여러 비동기 작업이 서로 연결되어 순서대로 실행되어야 할 때 발생합니다. 예를 들어, '사용자 정보 가져오기'가 끝나면 '그 사용자의 게시글 가져오기'를 해야 하고, 다시 '그 게시글의 댓글 가져오기'를 해야 한다고 생각해 보세요. 각 단계가 이전 단계의 결과에 의존하기 때문에, 콜백 함수 안에 또 다른 콜백 함수를 계속 넣는 방식으로 코드를 작성하게 됩니다.

    😈 콜백 헬의 실제 모습: 코드가 오른쪽으로... 오른쪽으로...

    아래 코드는 콜백 헬이 어떤 모습인지 잘 보여줍니다. 코드를 읽어 내려가다 보면, 점점 오른쪽으로 들여쓰기가 깊어지는 것을 볼 수 있습니다.

    
    // 🚨 비동기 작업을 흉내 낸 가상의 함수들입니다.
    // 실제로는 인터넷 통신이나 파일 읽기 등이 될 수 있습니다.
    
    // 1. 사용자 정보를 가져오는 함수
    function getUser(userId, callback) {
        // 1초 뒤에 콜백 함수를 호출합니다.
        setTimeout(() => {
            console.log(`1초 후: 1. 사용자 ID: ${userId} 정보 가져옴`);
            // 에러 없이 사용자 정보를 콜백에 전달
            callback(null, { id: userId, name: '앨리스' });
        }, 1000);
    }
    
    // 2. 특정 사용자의 게시글을 가져오는 함수
    function getPosts(userId, callback) {
        // 1초 뒤에 콜백 함수를 호출합니다.
        setTimeout(() => {
            console.log(`1초 후: 2. 사용자 ${userId}의 게시글 가져옴`);
            // 에러 없이 게시글 목록을 콜백에 전달
            callback(null, [{ postId: 101, title: '첫 번째 글' }, { postId: 102, title: '두 번째 글' }]);
        }, 1000);
    }
    
    // 3. 특정 게시글의 댓글을 가져오는 함수
    function getComments(postId, callback) {
        // 1초 뒤에 콜백 함수를 호출합니다.
        setTimeout(() => {
            console.log(`1초 후: 3. 게시글 ${postId}의 댓글 가져옴`);
            // 에러 없이 댓글 목록을 콜백에 전달
            callback(null, ['댓글 A', '댓글 B']);
        }, 1000);
    }
    
    // 😱 콜백 헬 예시: 비동기 작업이 순서대로 중첩될 때...
    getUser(1, (err, user) => { // 1단계: 사용자 정보 가져오기
        if (err) return console.error('사용자 정보 오류:', err); // 에러 발생 시 처리
        console.log(`사용자 이름: ${user.name}`);
    
        getPosts(user.id, (err, posts) => { // 2단계: 사용자 게시글 가져오기 (1단계 안에 중첩)
            if (err) return console.error('게시글 오류:', err); // 에러 발생 시 처리
            console.log(`첫 게시글 제목: ${posts[0].title}`);
    
            getComments(posts[0].postId, (err, comments) => { // 3단계: 게시글 댓글 가져오기 (2단계 안에 중첩)
                if (err) return console.error('댓글 오류:', err); // 에러 발생 시 처리
                console.log(`최종 확인된 첫 게시글 댓글:`, comments);
    
                // 😱 만약 여기에 4단계, 5단계 작업이 더 있다면...
                // 코드는 계속 오른쪽으로, 오른쪽으로... 지옥이 시작됩니다!
            });
        });
    });
      

    이처럼 코드가 오른쪽으로 깊게 들어가면서 다음과 같은 문제점들이 생깁니다.

    • 읽기 어려움: 코드를 한눈에 파악하기 어렵고, 비동기 작업의 실제 흐름을 추적하기 힘듭니다.
    • 고치기 어려움: 특정 부분을 수정하려면 다른 중첩된 콜백 함수들에 어떤 영향을 미칠지 예측하기 힘들어 버그가 생길 가능성이 높습니다.
    • 에러 처리 복잡: 각 콜백마다 일일이 에러를 확인하고 처리해야 하므로, 에러를 놓치거나 중복 처리하게 될 수 있습니다.
    • 재사용성 낮음: 깊게 묶인 코드는 다른 곳에서 재사용하기가 거의 불가능합니다.

    ✨ 콜백 헬을 벗어나는 마법 같은 해결 전략

    JavaScript는 이러한 콜백 헬의 고통에서 벗어나기 위한 강력한 도구들을 제공합니다. 대표적으로 세 가지 방법이 있습니다.

    1. Named Function (이름 있는 함수)으로 분리하기

    가장 쉽고 기본적인 방법입니다. 콜백 함수를 익명으로 바로 작성하지 않고, 별도의 이름 있는 함수로 만들어서 사용하는 것입니다. 이렇게 하면 코드의 들여쓰기가 줄어들어 가독성이 좋아집니다.

    
    // 1. 사용자 정보를 처리하는 함수
    function handleUser(err, user) {
        if (err) return console.error('사용자 정보 오류:', err);
        console.log(`사용자 이름: ${user.name}`);
        getPosts(user.id, handlePosts); // 다음 함수 호출
    }
    
    // 2. 게시글 정보를 처리하는 함수
    function handlePosts(err, posts) {
        if (err) return console.error('게시글 오류:', err);
        console.log(`첫 게시글 제목: ${posts[0].title}`);
        getComments(posts[0].postId, handleComments); // 다음 함수 호출
    }
    
    // 3. 댓글 정보를 처리하는 함수
    function handleComments(err, comments) {
        if (err) return console.error('댓글 오류:', err);
        console.log(`최종 확인된 첫 게시글 댓글:`, comments);
        // 더 이상의 작업이 없다면 여기서 끝!
    }
    
    // 🎉 이름 있는 함수로 분리하여 코드 실행
    getUser(1, handleUser);
      

    어떤가요? 아까보다 코드가 훨씬 깔끔하고 읽기 편해졌죠? 하지만 여전히 비동기 작업의 순서는 콜백에 의존하고, 에러 처리도 각 함수마다 해주어야 하는 한계가 있습니다.

    2. Promise (프로미스) 활용하기: 비동기 작업의 약속

    Promise는 JavaScript에서 '비동기 작업이 미래에 완료될 것인지, 아니면 실패할 것인지'를 나타내는 특별한 객체입니다. 콜백 헬의 복잡함을 해결하기 위해 등장했으며, 비동기 작업을 순서대로 '체인(Chain)'처럼 연결하여 코드의 흐름을 훨씬 읽기 좋게 만듭니다.

    • .then() 메서드: Promise가 성공적으로 완료(resolve)되었을 때 실행될 코드를 연결합니다. 마치 '이거 끝나면 다음은 이거 해줘'라고 약속하는 것과 같아요.
    • .catch() 메서드: Promise가 실패(reject)했거나, 중간에 에러가 발생했을 때 실행될 코드를 등록합니다. 체인 마지막에 .catch()를 한 번만 사용하면, 이전 모든 단계에서 발생한 에러를 한 곳에서 깔끔하게 처리할 수 있습니다.
    
    // ✅ Promise를 반환하도록 비동기 함수들을 새롭게 만듭니다.
    // 이제 함수들이 미래에 성공 또는 실패를 알려주는 '약속(Promise)'을 돌려줍니다.
    
    function getUserPromise(userId) {
        return new Promise((resolve, reject) => { // Promise를 새로 만듭니다.
            setTimeout(() => {
                console.log(`1초 후: 1. 사용자 ID: ${userId} 정보 가져옴 (Promise)`);
                if (userId === 999) { // 만약 ID가 999면 에러를 발생시키는 상황을 가정
                    return reject('사용자를 찾을 수 없습니다.'); // 실패했음을 알림 (에러 발생)
                }
                resolve({ id: userId, name: '앨리스' }); // 성공했음을 알림 (결과 전달)
            }, 1000);
        });
    }
    
    function getPostsPromise(userId) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`1초 후: 2. 사용자 ${userId}의 게시글 가져옴 (Promise)`);
                if (userId === 100) { // 만약 ID가 100이면 에러를 발생시키는 상황을 가정
                    return reject('게시글을 불러올 수 없습니다.');
                }
                resolve([{ postId: 101, title: '첫 번째 글' }, { postId: 102, title: '두 번째 글' }]);
            }, 1000);
        });
    }
    
    function getCommentsPromise(postId) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(`1초 후: 3. 게시글 ${postId}의 댓글 가져옴 (Promise)`);
                if (postId === 0) { // 만약 postId가 0이면 에러를 발생시키는 상황을 가정
                    return reject('댓글을 불러올 수 없습니다.');
                }
                resolve(['댓글 A', '댓글 B']);
            }, 1000);
        });
    }
    
    // 🎉 Promise 체이닝으로 콜백 헬 해결: 마치 엑셀 함수처럼 연결!
    getUserPromise(1) // 1. 사용자 정보 가져오는 약속 시작!
        .then(user => { // 약속이 성공하면, user 정보를 받아서 다음 작업 진행
            console.log(`사용자: ${user.name}`);
            return getPostsPromise(user.id); // 2. 게시글 가져오는 다음 약속을 반환!
        })
        .then(posts => { // 이전 약속(게시글 가져오기)이 성공하면, posts 정보를 받아서 다음 작업 진행
            console.log(`첫 게시글 제목: ${posts[0].title}`);
            return getCommentsPromise(posts[0].postId); // 3. 댓글 가져오는 다음 약속을 반환!
        })
        .then(comments => { // 이전 약속(댓글 가져오기)이 성공하면, comments 정보를 받아서 최종 처리
            console.log(`최종 확인된 댓글:`, comments);
        })
        .catch(error => { // ⚠️ 중요! Promise 체인에서 발생한 모든 에러를 이 한 곳에서 처리합니다.
            console.error('오류가 발생했습니다:', error);
        });
    
    // ❌ 에러 발생 시 Promise 체인이 어떻게 작동하는지 테스트해 보세요.
    // 아래 코드를 주석 해제하고 실행해 보면, .catch()에서 에러를 잡는 것을 볼 수 있습니다.
    // getUserPromise(999) // 존재하지 않는 사용자 ID로 에러 시뮬레이션
    //     .then(user => getPostsPromise(user.id))
    //     .then(posts => getCommentsPromise(posts[0].postId))
    //     .then(comments => console.log('최종 댓글:', comments))
    //     .catch(error => console.error('에러 시뮬레이션 결과:', error));
      

    Promise를 사용하면 코드가 훨씬 더 선형적(직선적)으로 바뀌어 가독성이 좋아집니다. 또한, 모든 에러를 .catch() 블록에서 한 번에 처리할 수 있어 에러 핸들링도 매우 편리해집니다.

    3. Async/Await (가장 현대적이고 쉬운 해결책)

    async/await은 JavaScript가 더욱 발전하여 ES2017에 도입된 문법입니다. Promise를 기반으로 만들어졌지만, 비동기 코드를 마치 일반적인 동기 코드처럼(순서대로 착착 실행되는 것처럼) 작성할 수 있게 해 주어 가독성을 최대치로 끌어올립니다. 콜백 헬을 해결하는 가장 우아하고 현대적인 방법이라고 할 수 있습니다.

    • async 키워드: 함수 앞에 async를 붙이면, 이 함수는 항상 Promise를 반환하는 특별한 비동기 함수가 됩니다. 이 함수 안에서 await를 사용할 수 있게 해주는 마법의 키워드입니다.
    • await 키워드: async 함수 안에서만 사용할 수 있습니다. 이 키워드를 Promise 앞에 붙이면, 해당 Promise가 완료될 때까지 잠시 기다렸다가 다음 코드를 실행합니다. 마치 동기 코드처럼 한 줄 한 줄 기다리면서 실행되는 것처럼 보이죠. 만약 Promise가 실패(reject)하면, await는 에러를 던지게 됩니다.
    
    // ✅ Promise를 반환하는 함수들을 다시 사용합니다.
    // (위에 정의했던 getUserPromise, getPostsPromise, getCommentsPromise 함수들)
    
    async function fetchAllUserData() { // 이 함수는 비동기 함수임을 명시합니다.
        try { // ⚠️ 중요! async/await에서 에러는 try-catch 문으로 잡습니다.
            console.log('--- Async/Await로 데이터 가져오기 시작 ---');
    
            const user = await getUserPromise(1); // 1. 사용자 정보를 가져올 때까지 기다립니다.
            console.log(`사용자: ${user.name}`);
    
            const posts = await getPostsPromise(user.id); // 2. 게시글을 가져올 때까지 기다립니다.
            console.log(`게시글: ${posts[0].title}`);
    
            const comments = await getCommentsPromise(posts[0].postId); // 3. 댓글을 가져올 때까지 기다립니다.
            console.log(`최종 확인된 댓글:`, comments);
    
            console.log('--- 모든 데이터 가져오기 완료 ---');
    
        } catch (error) { // ➡️ 비동기 작업 중 발생한 에러를 이곳에서 동기 코드처럼 처리합니다.
            console.error('앗, 오류가 발생했어요!:', error);
        }
    }
    
    fetchAllUserData(); // 비동기 함수 실행
    
    // ❌ 에러 발생 시 async/await가 어떻게 작동하는지 테스트해 보세요.
    // 아래 코드를 주석 해제하고 실행해 보면, try-catch에서 에러를 잡는 것을 볼 수 있습니다.
    // async function fetchUserDataWithErrorExample() {
    //     try {
    //         console.log('--- 에러 발생 Async/Await 테스트 시작 ---');
    //         const user = await getUserPromise(999); // 존재하지 않는 사용자 ID로 에러 발생 유도
    //         console.log(`사용자: ${user.name}`); // 이 코드는 실행되지 않습니다.
    //     } catch (error) {
    //         console.error('Async/Await 에러 발생 시뮬레이션:', error);
    //     }
    // }
    // fetchUserDataWithErrorExample();
      

    async/await을 사용하면 코드가 마치 소설을 읽듯이 자연스럽게 위에서 아래로 흐르는 것처럼 보입니다. 복잡한 비동기 로직도 동기 코드처럼 쉽게 이해하고 관리할 수 있어 콜백 헬의 악몽에서 완벽하게 벗어날 수 있습니다. 현대 JavaScript 개발에서는 이 방식을 가장 많이 사용합니다.

    ✔️ 결론: 콜백 헬, 이제 안녕!

    JavaScript의 콜백 헬은 비동기 프로그래밍의 흔한 문제이지만, 이제 우리는 이를 극복할 수 있는 훌륭한 도구들을 알고 있습니다.

    • 코드 구조를 개선하는 '이름 있는 함수(Named Function)' 분리
    • 비동기 작업의 약속을 체인처럼 연결하는 'Promise'
    • Promise를 더욱 읽기 쉽게 만들어주는 마법의 문법 'async/await'

    이 중에서 특히 Promise와 async/await은 현대 JavaScript 비동기 프로그래밍의 핵심입니다. 이 두 가지를 능숙하게 사용한다면, 아무리 복잡한 비동기 작업이라도 깔끔하고 효율적으로 처리할 수 있게 될 것입니다. 이제 더 이상 콜백 헬에 갇히지 말고, 멋진 비동기 코드를 작성해 보세요!

    JS 웹 스토리지 완전 정복: LocalStorage & SessionStorage 활용법과 예제(Mastering Web Storage: LocalStorage & SessionStorage Usage and Examples)

    웹 스토리지 완전 정복: LocalStorage & SessionStorage 활용법과 예제

    현대 웹 애플리케이션은 사용자 경험을 향상시키기 위해 다양한 데이터를 클라이언트 측(브라우저)에 저장합니다. 로그인 상태 유지, 장바구니 정보, 사용자 설정 등 많은 정보가 웹 페이지를 닫거나 다시 방문했을 때도 유지되어야 합니다. 이때 핵심적인 역할을 하는 것이 바로 **웹 스토리지(Web Storage)**입니다. 이 포스팅에서는 웹 스토리지의 두 가지 주요 유형인 **LocalStorage**와 **SessionStorage**의 개념, 사용법, 그리고 실제 활용 예시를 상세히 알아보겠습니다.

    🧩 웹 스토리지란?

    웹 스토리지(Web Storage API)는 웹 애플리케이션이 사용자 브라우저 내에 데이터를 로컬로 저장할 수 있는 방법을 제공하는 기술입니다. 기존에 클라이언트 측 데이터를 저장하던 쿠키(Cookies)의 단점을 보완하며 등장했습니다.

    웹 스토리지가 쿠키보다 좋은 점

    • 더 큰 저장 용량: 쿠키는 약 4KB의 제한된 용량을 가지지만, 웹 스토리지는 도메인당 약 5MB 이상(브라우저마다 다름)의 훨씬 큰 용량을 제공합니다. 이 정도면 대부분의 클라이언트 측 데이터를 저장하기에 충분합니다.
    • 서버 전송 없음: 쿠키는 모든 HTTP 요청 시 자동으로 서버로 전송되어 네트워크 트래픽을 증가시키고 성능에 영향을 줄 수 있습니다. 웹 스토리지 데이터는 명시적으로 요청하지 않는 한 서버로 전송되지 않으므로, 네트워크 부하가 줄어듭니다.
    • 간단한 API: 쿠키는 복잡한 파싱 및 직렬화 과정이 필요한 반면, 웹 스토리지는 setItem(), getItem() 등 직관적이고 쉬운 JavaScript API를 제공합니다.

    📚 LocalStorage와 SessionStorage의 차이점

    웹 스토리지는 데이터를 저장하는 방식과 생명 주기에 따라 크게 두 가지 객체로 나뉩니다.

    • LocalStorage (로컬 스토리지)

      • 영구적인 저장: LocalStorage에 저장된 데이터는 사용자가 브라우저를 닫거나 컴퓨터를 재시작해도 **영구적으로 유지**됩니다.
      • 삭제 방법: 명시적으로 JavaScript 코드를 통해 삭제하거나, 브라우저 설정에서 캐시/사이트 데이터를 지워야만 삭제됩니다.
      • 활용 예시: 자동 로그인 정보, 사용자 테마 설정(다크 모드/라이트 모드), 장바구니 정보, 사용자 기본 설정 등 웹사이트를 재방문해도 유지되어야 하는 데이터 저장.
    • SessionStorage (세션 스토리지)

      • 세션 기간 동안 저장: SessionStorage에 저장된 데이터는 **현재 브라우저 세션(탭 또는 창)이 유지되는 동안에만 존재**합니다.
      • 삭제 시점: 사용자가 해당 탭이나 창을 닫으면 데이터는 자동으로 삭제됩니다. 새로고침 시에는 유지됩니다.
      • 활용 예시: 일회성 로그인 상태, 결제 과정 중 임시 데이터, 비로그인 상태의 장바구니 (페이지 이동 시 유지, 브라우저 닫으면 삭제), 일시적인 사용자 입력값 등 세션이 종료되면 사라져야 하는 데이터 저장.

    간단 비교표

    특징 LocalStorage SessionStorage
    데이터 유지 기간 영구적 (사용자가 직접 삭제 또는 브라우저 데이터 삭제 시까지) 브라우저 세션(탭/창)이 유지되는 동안
    다른 탭/창 접근 동일 출처(Origin)의 다른 탭/창에서 접근 가능 동일 출처 내에서도 다른 탭/창에서 접근 불가능 (각 세션은 독립적)
    데이터 전송 서버로 자동 전송되지 않음 서버로 자동 전송되지 않음
    저장 용량 약 5MB 이상 약 5MB 이상
    주요 사용 사례 자동 로그인, 사용자 설정, 장바구니 등 일시적 로그인, 결제 정보 등 세션 한정 데이터

    ⚙️ 웹 스토리지 공통 API 사용법

    LocalStorage와 SessionStorage는 동일한 API를 사용하므로, 사용법을 한 번 익히면 두 가지 모두에 적용할 수 있습니다. 데이터를 저장할 때는 키(key)와 값(value)의 형태로 저장하며, **값은 반드시 문자열(String) 형태**여야 합니다. 객체나 배열 같은 복잡한 데이터를 저장하려면 JSON 문자열로 변환해야 합니다.

    • 데이터 저장: setItem(key, value)

      지정된 키에 값을 저장합니다. 값이 문자열이 아니라면 자동으로 문자열로 변환되지만, 객체나 배열은 `JSON.stringify()`를 사용하는 것이 안전합니다.

      
      // LocalStorage에 문자열 저장
      localStorage.setItem('username', 'codingdoitwolf');
      
      // SessionStorage에 숫자 저장 (자동으로 문자열 "123"으로 변환됨)
      sessionStorage.setItem('userId', 123);
      
      // LocalStorage에 객체 저장 (JSON.stringify() 사용 필수!)
      const userSettings = { theme: 'dark', notifications: true };
      localStorage.setItem('settings', JSON.stringify(userSettings));
      
      // SessionStorage에 배열 저장 (JSON.stringify() 사용 필수!)
      const cartItems = ['itemA', 'itemB'];
      sessionStorage.setItem('cart', JSON.stringify(cartItems));
            
    • 데이터 가져오기: getItem(key)

      지정된 키에 해당하는 값을 가져옵니다. 값이 없으면 null을 반환합니다. 객체나 배열로 저장된 데이터는 `JSON.parse()`를 사용하여 다시 객체/배열로 변환해야 합니다.

      
      // LocalStorage에서 문자열 가져오기
      const username = localStorage.getItem('username'); // 'codingdoitwolf'
      
      // SessionStorage에서 숫자 가져오기 (문자열 "123"으로 가져와짐)
      const userId = sessionStorage.getItem('userId'); // "123" (문자열)
      const parsedUserId = parseInt(userId); // 숫자로 변환
      
      // LocalStorage에서 객체 가져오기 (JSON.parse() 사용 필수!)
      const settingsString = localStorage.getItem('settings');
      const userSettings = JSON.parse(settingsString);
      // userSettings는 { theme: 'dark', notifications: true } 객체가 됨
      
      // SessionStorage에서 배열 가져오기 (JSON.parse() 사용 필수!)
      const cartString = sessionStorage.getItem('cart');
      const cartItems = JSON.parse(cartString);
      // cartItems는 ['itemA', 'itemB'] 배열이 됨
            
    • 데이터 삭제: removeItem(key)

      지정된 키에 해당하는 데이터를 삭제합니다.

      
      // LocalStorage에서 'username' 데이터 삭제
      localStorage.removeItem('username');
      
      // SessionStorage에서 'userId' 데이터 삭제
      sessionStorage.removeItem('userId');
            
    • 모든 데이터 삭제: clear()

      해당 스토리지(LocalStorage 또는 SessionStorage)에 저장된 모든 데이터를 삭제합니다.

      
      // LocalStorage의 모든 데이터 삭제
      localStorage.clear();
      
      // SessionStorage의 모든 데이터 삭제
      sessionStorage.clear();
            
    • 키 가져오기: key(index)

      지정된 인덱스에 해당하는 키(key) 이름을 가져옵니다. 모든 저장된 키를 반복할 때 유용합니다.

      
      // LocalStorage에 저장된 첫 번째 키 가져오기
      const firstKey = localStorage.key(0);
      
      // LocalStorage에 저장된 모든 키와 값 출력
      for (let i = 0; i < localStorage.length; i++) {
          const key = localStorage.key(i);
          const value = localStorage.getItem(key);
          console.log(`Key: ${key}, Value: ${value}`);
      }
            
    • 저장된 데이터 개수: length

      해당 스토리지에 저장된 데이터(키-값 쌍)의 개수를 반환합니다.

      
      const numberOfItems = localStorage.length; // LocalStorage에 저장된 항목 수
            

    💡 웹 스토리지 활용 예제

    실제 웹 개발에서 LocalStorage와 SessionStorage가 어떻게 활용될 수 있는지 구체적인 예제를 통해 살펴보겠습니다.

    예제 1: 사용자 다크 모드 설정 저장 (LocalStorage)

    사용자가 다크 모드를 선택하면 그 설정을 LocalStorage에 저장하여, 브라우저를 닫았다가 다시 열어도 설정이 유지되도록 합니다.

    
    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="UTF-8">
      <title>다크 모드 설정 예제</title>
      <style>
        body { font-family: sans-serif; transition: background-color 0.3s, color 0.3s; }
        body.dark-mode { background-color: #333; color: #f2f2f2; }
        .container { max-width: 600px; margin: 50px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
      </style>
    </head>
    <body>
      <div class="container">
        <h2>다크 모드 설정</h2>
        <button id="toggleDarkMode">다크 모드 전환</button>
        <p>이 페이지를 닫았다가 다시 열어도 설정이 유지됩니다.</p>
      </div>
    
      <script>
        const toggleButton = document.getElementById('toggleDarkMode');
        const body = document.body;
    
        // 페이지 로드 시 LocalStorage에서 설정 불러오기
        const savedMode = localStorage.getItem('themeMode');
        if (savedMode === 'dark') {
          body.classList.add('dark-mode');
        }
    
        // 버튼 클릭 시 모드 전환 및 LocalStorage에 저장
        toggleButton.addEventListener('click', () => {
          if (body.classList.contains('dark-mode')) {
            body.classList.remove('dark-mode');
            localStorage.setItem('themeMode', 'light');
          } else {
            body.classList.add('dark-mode');
            localStorage.setItem('themeMode', 'dark');
          }
        });
      </script>
    </body>
    </html>
      

    예제 2: 임시 장바구니 데이터 저장 (SessionStorage)

    사용자가 상품을 장바구니에 담았을 때, 다른 페이지로 이동해도 장바구니 내용이 유지되지만, 브라우저 탭을 닫으면 사라지게 하고 싶을 때 SessionStorage를 사용할 수 있습니다.

    
    <!DOCTYPE html>
    <html lang="ko">
    <head>
      <meta charset="UTF-8">
      <title>임시 장바구니 예제</title>
      <style>
        .container { max-width: 600px; margin: 50px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
        #cartList { list-style: none; padding: 0; }
        #cartList li { margin-bottom: 5px; }
      </style>
    </head>
    <body>
      <div class="container">
        <h2>임시 장바구니</h2>
        <input type="text" id="itemName" placeholder="상품명">
        <button id="addItem">상품 추가</button>
        <button id="clearCart">장바구니 비우기</button>
        <h3>장바구니 목록:</h3>
        <ul id="cartList"></ul>
        <p><small>브라우저 탭을 닫으면 장바구니 내용이 사라집니다.</small></p>
      </div>
    
      <script>
        const itemNameInput = document.getElementById('itemName');
        const addItemButton = document.getElementById('addItem');
        const clearCartButton = document.getElementById('clearCart');
        const cartList = document.getElementById('cartList');
    
        // SessionStorage에서 장바구니 불러와 화면에 표시
        function loadCart() {
          cartList.innerHTML = ''; // 목록 초기화
          const savedCart = sessionStorage.getItem('currentCart');
          const items = savedCart ? JSON.parse(savedCart) : [];
    
          items.forEach(item => {
            const li = document.createElement('li');
            li.textContent = item;
            cartList.appendChild(li);
          });
        }
    
        // 페이지 로드 시 장바구니 불러오기
        loadCart();
    
        // 상품 추가 버튼 클릭 이벤트
        addItemButton.addEventListener('click', () => {
          const newItem = itemNameInput.value.trim();
          if (newItem) {
            const savedCart = sessionStorage.getItem('currentCart');
            const items = savedCart ? JSON.parse(savedCart) : [];
            items.push(newItem);
            sessionStorage.setItem('currentCart', JSON.stringify(items));
            itemNameInput.value = ''; // 입력 필드 초기화
            loadCart(); // 장바구니 업데이트
          }
        });
    
        // 장바구니 비우기 버튼 클릭 이벤트
        clearCartButton.addEventListener('click', () => {
          sessionStorage.clear(); // SessionStorage의 모든 데이터 삭제
          loadCart(); // 장바구니 업데이트
        });
      </script>
    </body>
    </html>
      

    ⚠️ 웹 스토리지 사용 시 주의사항

    웹 스토리지는 편리하지만, 몇 가지 주의할 점이 있습니다.

    • 보안: 웹 스토리지는 XSS(Cross-Site Scripting) 공격에 취약할 수 있습니다. 중요한 민감 정보(예: 비밀번호, 신용카드 번호)는 웹 스토리지에 직접 저장해서는 안 됩니다. 서버에서 안전하게 관리하거나 암호화된 형태로 저장하고, 반드시 HTTPS를 사용해야 합니다.
    • 동기적(Synchronous) 작업: 웹 스토리지의 모든 작업(setItem, getItem 등)은 동기적으로 수행됩니다. 이는 데이터를 읽고 쓰는 동안 메인 스레드가 차단될 수 있다는 의미입니다. 매우 큰 데이터를 저장하거나 자주 접근할 경우 웹 애플리케이션의 성능에 영향을 줄 수 있으니 주의해야 합니다.
    • 문자열만 저장 가능: 앞에서 언급했듯이, 웹 스토리지는 문자열만 저장할 수 있습니다. 객체나 배열을 저장하려면 `JSON.stringify()`로 직렬화(Serialization)하고, 가져올 때는 `JSON.parse()`로 역직렬화(Deserialization)해야 합니다.
    • 만료 기한 없음: LocalStorage는 쿠키와 달리 만료 기한을 설정할 수 없습니다. 따라서 불필요한 데이터는 removeItem()이나 clear()를 통해 직접 삭제해야 합니다. SessionStorage는 탭/창 종료 시 자동 삭제되므로 이 문제는 해당되지 않습니다.
    • 용량 제한: 브라우저마다 다르지만, 도메인당 5MB~10MB 정도의 용량 제한이 있습니다. 이보다 더 큰 데이터를 저장해야 한다면 IndexedDB 같은 다른 클라이언트 측 스토리지 솔루션을 고려해야 합니다.

    ✨ 결론

    웹 스토리지는 클라이언트 측 데이터를 효율적으로 관리하고 사용자 경험을 향상시키는 데 매우 유용한 도구입니다. 특히 LocalStorage는 영구적인 데이터 저장에, SessionStorage는 세션 기반의 임시 데이터 저장에 적합합니다.

    이 포스팅에서 다룬 API 사용법과 예시를 통해 웹 스토리지의 강력함을 이해하고 실제 프로젝트에 효과적으로 적용하시길 바랍니다. 다만, 보안 및 성능상의 주의사항을 항상 염두에 두시고 적절한 상황에 맞는 스토리지 솔루션을 선택하는 것이 중요합니다.

    📢 자바스크립트로 오른쪽에서 왼쪽으로 흐르는 문자 만들기(JavaScript Right-to-Left Marquee Text)

    📢 자바스크립트로 오른쪽에서 왼쪽으로 흐르는 문자 만들기 뉴스 속보나 광고 배너처럼, 웹페이지 상단 또는 중간에 문자가 오른쪽에서 왼쪽으로 자연스럽게 흐르는 효과 를 구현하고 싶을 때가 있습니다. 자바스크립트와 CSS를 조합하면 매우...