Skip to main content
Summary

이제 조각을 하나로 묶을 차례입니다

지금까지 변수, 조건문, 반복문, 함수, 배열, 객체, DOM, 이벤트, 저장소, fetch()를 따로 배웠습니다. 따로 배운 개념은 처음에는 흩어진 도구처럼 느껴집니다. 하지만 작은 프로젝트를 만들기 시작하면 각 도구가 어디에 쓰이는지 조금씩 분명해집니다.

이번 장의 목표는 "완벽한 앱"이 아닙니다. 직접 입력하고, 화면이 바뀌고, 새로고침해도 일부 내용이 남고, 외부 데이터를 가져오는 경험을 해 보는 것입니다. 이 경험이 있으면 다음에 더 큰 기술을 배울 때도 겁이 줄어듭니다.

먼저 project.html 파일을 하나 만들고 아래 기본 구조를 붙여 넣으세요. 이후 실습 코드는 모두 이 파일의 <script> 태그 안에 차례로 추가하면서 확인하면 됩니다. 처음부터 전부 이해하려고 하기보다, 한 프로젝트씩 붙여 넣고 실행해 보세요.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>나의 자바스크립트 프로젝트</title>
  <style>
    body { font-family: sans-serif; max-width: 720px; margin: 40px auto; }
    section { border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; border-radius: 8px; }
    button { margin-left: 4px; }
    .done { text-decoration: line-through; color: gray; }
    .card { background: #f7f7f7; padding: 12px; margin-top: 8px; border-radius: 8px; }
  </style>
</head>
<body>
  <h1>나의 작은 자바스크립트 프로젝트</h1>

  <section>
    <h2>할 일 목록</h2>
    <form id="todoForm">
      <input id="todoInput" placeholder="할 일을 입력하세요" />
      <button>추가</button>
    </form>
    <ul id="todoList"></ul>
  </section>

  <section>
    <h2>점심 메뉴 추천</h2>
    <button id="menuButton">추천받기</button>
    <p id="menuResult">버튼을 눌러 보세요.</p>
  </section>

  <section>
    <h2>디지털 시계</h2>
    <p id="clock">--:--:--</p>
  </section>

  <section>
    <h2>사용자 카드</h2>
    <button id="loadUsersButton">데이터 가져오기</button>
    <div id="userCards"></div>
  </section>

  <script>
    // 여기에 자바스크립트를 작성합니다.
  </script>
</body>
</html>

작은 프로젝트로 자바스크립트 완성하기AI 생성 이미지

프로젝트 1: 나만의 할 일 목록 만들기

할 일 목록은 앞에서 배운 대부분의 개념이 들어가는 좋은 연습입니다. 입력값을 읽고, 배열에 저장하고, 화면에 그리고, 클릭하면 상태를 바꾸고, 삭제도 해야 합니다. 마지막에는 localStorage로 새로고침 후에도 할 일이 남게 만들겠습니다.

먼저 <script> 안에 아래 코드를 넣으세요.

const todoForm = document.querySelector('#todoForm');
const todoInput = document.querySelector('#todoInput');
const todoList = document.querySelector('#todoList');

let todos = JSON.parse(localStorage.getItem('todos')) || [];

function saveTodos() {
  localStorage.setItem('todos', JSON.stringify(todos));
}

function renderTodos() {
  todoList.innerHTML = '';

  todos.forEach(function (todo, index) {
    const li = document.createElement('li');
    li.textContent = todo.text;

    if (todo.done) {
      li.classList.add('done');
    }

    const doneButton = document.createElement('button');
    doneButton.textContent = '완료';
    doneButton.addEventListener('click', function () {
      todos[index].done = !todos[index].done;
      saveTodos();
      renderTodos();
    });

    const deleteButton = document.createElement('button');
    deleteButton.textContent = '삭제';
    deleteButton.addEventListener('click', function () {
      todos.splice(index, 1);
      saveTodos();
      renderTodos();
    });

    li.appendChild(doneButton);
    li.appendChild(deleteButton);
    todoList.appendChild(li);
  });
}

todoForm.addEventListener('submit', function (event) {
  event.preventDefault();

  const text = todoInput.value.trim();

  if (text === '') {
    alert('할 일을 입력해 주세요.');
    return;
  }

  todos.push({ text: text, done: false });
  todoInput.value = '';

  saveTodos();
  renderTodos();
});

renderTodos();

이 코드는 길어 보이지만 역할을 나누면 단순합니다.

  • todos는 할 일 객체들을 담는 배열입니다.

  • saveTodos()는 배열을 문자열로 바꿔 localStorage에 저장합니다.

  • renderTodos()는 현재 배열을 기준으로 화면을 다시 그립니다.

  • submit 이벤트는 입력값을 받아 새 할 일을 추가합니다.

  • 완료 버튼은 done 값을 true와 false 사이에서 바꿉니다.

  • 삭제 버튼은 splice()로 배열에서 해당 항목을 제거합니다.

여기서 중요한 흐름은 "데이터를 바꾼 뒤 화면을 다시 그린다"입니다. 초보 단계에서는 이 흐름만 익혀도 충분합니다.

특히 renderTodos() 안에서 todoList.innerHTML = '';를 먼저 실행하는 부분을 눈여겨보세요. 기존 목록을 비운 뒤, 현재 todos 배열을 기준으로 다시 만드는 방식입니다. 그래서 추가, 완료, 삭제가 일어날 때마다 화면이 현재 데이터와 맞게 정리됩니다.

자주 나는 오류도 확인해 봅시다. Cannot read properties of null이 보이면 보통 선택자를 잘못 썼거나, HTML에 해당 id가 없습니다. 예를 들어 #todoForm을 찾는데 HTML에는 id="todoform"처럼 대소문자가 다르면 찾지 못합니다. 이때는 console.log(todoForm);을 찍어 실제로 요소를 찾았는지 먼저 확인하세요.

프로젝트 2: 랜덤 추천기로 배열과 조건문 복습하기

두 번째 프로젝트는 점심 메뉴 추천기입니다. 배열에서 무작위 항목을 하나 고르는 기능입니다. 여기에 조건문을 살짝 넣어, 추천 결과에 따라 다른 문구를 보여 주겠습니다.

const menuButton = document.querySelector('#menuButton');
const menuResult = document.querySelector('#menuResult');

const menus = ['김밥', '비빔밥', '샌드위치', '쌀국수', '돈가스'];

menuButton.addEventListener('click', function () {
  const randomIndex = Math.floor(Math.random() * menus.length);
  const selectedMenu = menus[randomIndex];

  if (selectedMenu === '샌드위치') {
    menuResult.textContent = `오늘은 가볍게 ${selectedMenu} 어때요?`;
  } else {
    menuResult.textContent = `오늘의 추천 메뉴는 ${selectedMenu}입니다.`;
  }
});

Math.random()은 0 이상 1 미만의 숫자를 만듭니다. 여기에 배열 길이를 곱하고 Math.floor()로 소수점을 버리면 배열 인덱스로 사용할 수 있습니다.

이 프로젝트를 명언 카드로 바꾸고 싶다면 menus 배열의 내용만 명언으로 바꿔도 됩니다.

const menus = [
  '작게 시작해도 괜찮습니다.',
  '오늘 고친 오류가 내일의 실력이 됩니다.',
  '완벽한 코드보다 실행되는 코드가 먼저입니다.'
];

물론 이때 버튼 이름과 화면 문구도 "명언 추천"에 어울리게 바꾸면 더 자연스럽습니다. 중요한 점은 배열 안의 데이터만 바꿔도 기능의 성격이 달라진다는 것입니다. 이것이 데이터와 동작을 분리하는 첫 감각입니다.

프로젝트 3: Date와 setInterval로 시계 만들기

이번에는 시간이 흐르면서 화면이 계속 바뀌는 기능을 만들겠습니다. Date는 현재 날짜와 시간을 알려 주고, setInterval()은 정해진 간격마다 함수를 반복 실행합니다.

const clock = document.querySelector('#clock');

function updateClock() {
  const now = new Date();

  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');

  clock.textContent = `${hours}:${minutes}:${seconds}`;
}

updateClock();
setInterval(updateClock, 1000);

padStart(2, '0')은 글자의 길이가 2보다 짧으면 앞에 0을 붙입니다. 그래서 9:3:5가 아니라 09:03:05처럼 보기 좋은 시계가 됩니다.

여기서도 흐름은 간단합니다.

  1. 현재 시간을 가져옵니다.

  2. 화면에 표시할 문자열로 바꿉니다.

  3. DOM의 textContent를 바꿉니다.

  4. 1초마다 같은 일을 반복합니다.

마감 카운트다운을 만들고 싶다면 현재 시간과 마감 시간의 차이를 계산하면 됩니다.

const deadline = new Date('2026-12-31T23:59:59');
const diff = deadline.getTime() - new Date().getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));

처음부터 완성된 카운트다운을 만들려고 하기보다, console.log(days);로 남은 날짜가 맞게 나오는지 확인한 뒤 화면에 붙이세요. 만약 마감 시간이 이미 지났다면 diff가 음수가 될 수 있으니, 그런 경우에는 "마감되었습니다" 같은 문구를 보여 주도록 조건문을 추가하면 됩니다.

프로젝트 4: fetch와 JSON으로 사용자 카드 보여 주기

마지막 프로젝트는 인터넷에서 데이터를 가져와 화면에 카드로 보여 주는 기능입니다. 여기서는 연습용 공개 API인 JSONPlaceholder의 사용자 목록을 사용하겠습니다.

const loadUsersButton = document.querySelector('#loadUsersButton');
const userCards = document.querySelector('#userCards');

loadUsersButton.addEventListener('click', async function () {
  userCards.textContent = '데이터를 가져오는 중입니다...';

  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');

    if (!response.ok) {
      throw new Error('데이터 요청에 실패했습니다.');
    }

    const users = await response.json();
    userCards.innerHTML = '';

    users.slice(0, 3).forEach(function (user) {
      const card = document.createElement('div');
      card.classList.add('card');

      card.innerHTML = `
        <strong>${user.name}</strong>
        <p>이메일: ${user.email}</p>
        <p>도시: ${user.address.city}</p>
      `;

      userCards.appendChild(card);
    });
  } catch (error) {
    userCards.textContent = '데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.';
    console.log(error);
  }
});

이 코드는 서버에 요청하고, 응답을 JSON으로 바꾸고, 배열의 일부를 카드로 만듭니다. 앞 장에서 배운 try...catch도 다시 등장합니다. 인터넷 요청은 내 코드가 맞아도 실패할 수 있습니다. 연결이 끊겼거나, 주소가 틀렸거나, 서버가 응답하지 않을 수 있기 때문입니다.

여기서는 카드 모양을 쉽게 만들기 위해 innerHTML을 사용했습니다. 공개 API 예제 데이터를 화면에 보여 주는 연습으로는 괜찮지만, 실제 서비스에서 사용자가 입력한 값을 그대로 innerHTML에 넣는 습관은 조심해야 합니다. 처음에는 textContent가 더 안전하다는 점만 기억해 두세요.

오류가 나면 먼저 세 가지를 확인하세요.

  • 주소가 정확한가?

  • await response.json()을 빠뜨리지 않았는가?

  • 화면에 넣으려는 속성 이름이 실제 데이터와 같은가?

속성 이름이 헷갈리면 console.log(users[0]);을 찍어 첫 번째 데이터의 모양을 확인하세요. 모르는 데이터는 추측하지 말고 먼저 들여다보는 습관이 좋습니다.

완성 후 스스로 기능을 추가해 보기

프로젝트가 실행되면 바로 다음 기능을 하나만 골라 추가해 보세요. 많이 추가하려고 하면 금방 복잡해집니다.

할 일 목록에는 이런 기능을 붙일 수 있습니다.

  • 전체 삭제 버튼

  • 완료된 항목만 보기

  • 남은 할 일 개수 표시

  • 같은 할 일을 중복으로 추가하지 않기

  • Enter로 추가한 뒤 입력칸에 자동 포커스 주기

추천기에는 이런 기능을 붙일 수 있습니다.

  • 이미 추천된 메뉴는 다시 나오지 않게 하기

  • 한식, 양식, 간식 카테고리 나누기

  • 추천 기록을 localStorage에 저장하기

데이터 카드에는 이런 기능을 붙일 수 있습니다.

  • 검색어로 사용자 이름 필터링하기

  • 카드 클릭 시 자세한 정보 보여 주기

  • 로딩 중 버튼 비활성화하기

중요한 것은 기능을 추가하기 전에 한 문장으로 적는 것입니다.

"사용자가 버튼을 누르면 완료된 할 일만 보이게 한다."

이렇게 적으면 필요한 도구가 보입니다. 버튼이 필요하니 DOM과 이벤트가 필요합니다. 완료 여부를 봐야 하니 조건문이 필요합니다. 여러 할 일을 확인해야 하니 배열과 반복이 필요합니다.

다음 공부로 넘어가기 전 로드맵

이 책을 끝냈다고 해서 자바스크립트를 모두 배운 것은 아닙니다. 하지만 웹페이지를 직접 바꾸고, 사용자 입력에 반응하고, 데이터를 저장하고, 인터넷에서 정보를 가져오는 기본 흐름은 경험했습니다. 이제 다음 단계로 갈 준비가 되었습니다.

바로 라이브러리나 프레임워크로 넘어가도 되지만, 그전에 아래 내용을 조금 더 연습하면 좋습니다.

  • 함수를 더 작게 나누기

  • 배열 메서드 map, filter, find 익숙해지기

  • async, await, try...catch 반복 연습하기

  • DOM 요소를 만들고 지우는 흐름 다시 연습하기

  • 오류 메시지를 읽고 검색어로 바꾸는 연습하기

그다음에는 선택지가 넓어집니다.

  • React 같은 프레임워크: 화면이 자주 바뀌는 웹앱을 더 체계적으로 만들 때 배웁니다.

  • Node.js: 브라우저 밖에서 자바스크립트를 실행하고 서버를 만들 때 배웁니다.

  • TypeScript: 자바스크립트에 타입 정보를 더해 큰 코드의 실수를 줄이고 싶을 때 배웁니다.

무엇을 먼저 배울지는 만들고 싶은 것에 따라 다릅니다. 웹 화면을 더 잘 만들고 싶다면 React, 서버와 API를 만들고 싶다면 Node.js, 코드 규모가 커져서 실수를 줄이고 싶다면 TypeScript가 자연스러운 다음 단계입니다.

마지막으로 기억할 점은 하나입니다. 자바스크립트 실력은 문법을 외운 양보다, 작은 기능을 끝까지 만들어 본 횟수에 더 많이 영향을 받습니다. 오늘 만든 프로젝트를 복사해서 이름만 바꿔 보세요. 할 일 목록을 장보기 목록으로, 메뉴 추천기를 여행지 추천기로, 사용자 카드를 상품 카드로 바꿔 보세요. 같은 구조를 여러 번 다르게 써 볼 때, 코드가 내 도구가 되기 시작합니다.

책 목록

비개발자를 위한 JavaScript: 설치 없이 시작해 작은 프로젝트까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기