JavaScript 실무 실수 해설팩 01

자료 유형: 무료 실무 해설팩

JavaScript 기본 문법은 배웠지만 실제 화면, 폼, 목록, API 코드에서 자주 막히는 학습자를 위한 실무 해설팩입니다. 단순 문법 설명이 아니라 버그로 이어지는 JavaScript 실수를 어떻게 발견하고 고치는지 기준을 제공합니다.

이 팩의 약속

이 자료는 var, let, const 같은 이론 나열이 아니다. 실제 서비스 화면에서 자주 터지는 실수를 중심으로 설명한다.

  • 폼 입력값이 문자열이라 계산이 깨지는 문제
  • 배열 원본을 바꿔 목록 화면이 예측 불가능해지는 문제
  • 비동기 결과를 기다리지 않아 빈 화면 또는 이전 데이터가 보이는 문제
  • DOM 이벤트를 중복 등록해 버튼이 여러 번 실행되는 문제

각 예제는 흔한 실패 코드 -> 왜 위험한지 -> 실무 화면에서 어떻게 터지는지 -> 개선 코드 -> 코드 리뷰 체크포인트 -> 다시 풀 문제 순서로 본다.

예제 1. 숫자처럼 보이는 문자열 계산 버그

연결 문제: https://nst21c.com/?task=sum#labApp

흔한 실패 코드

function calculateTotal(price, count) {
  return price + count;
}

폼에서 받은 값은 대부분 문자열이다. price"10000"이고 count"2"이면 결과는 100002가 된다. 문제 풀이에서는 단순 덧셈 실수처럼 보이지만, 실무에서는 주문 금액, 수량, 할인 금액이 틀어지는 버그다.

왜 위험한가

JavaScript의 +는 숫자 덧셈과 문자열 연결을 모두 한다. 입력값의 출처가 input, URL query, localStorage, CSV, API라면 숫자처럼 보여도 문자열일 가능성이 높다.

개선 코드

function calculateTotal(price, count) {
  const numericPrice = Number(price);
  const numericCount = Number(count);

  if (!Number.isFinite(numericPrice) || !Number.isFinite(numericCount)) {
    return 0;
  }

  return numericPrice * numericCount;
}

실무 화면 예

  • 장바구니 총액
  • 관리자 매출 필터
  • 쿠폰 할인 계산
  • 페이지네이션 offset 계산

코드 리뷰 체크포인트

  • 폼 입력값을 계산 전에 숫자로 변환했는가?
  • Number() 결과가 NaN일 때 처리하는가?
  • 금액/수량 계산에서 문자열 연결이 일어날 여지가 없는가?

변형 과제

  • 빈 문자열이 들어오면 0으로 처리한다.
  • 음수 수량이 들어오면 에러 메시지를 반환한다.

예제 2. 원본 배열 변경으로 목록 화면이 흔들리는 버그

연결 문제: https://nst21c.com/?task=arraySum#labApp

흔한 실패 코드

function getRecentOrders(orders) {
  return orders.sort((a, b) => b.createdAt - a.createdAt).slice(0, 5);
}

sort()는 원본 배열을 바꾼다. 한 화면에서는 최신순으로 잘 보이지만, 다른 컴포넌트나 다음 필터에서 원래 순서를 기대하면 목록이 갑자기 바뀐다.

개선 코드

function getRecentOrders(orders) {
  return [...orders]
    .sort((a, b) => b.createdAt - a.createdAt)
    .slice(0, 5);
}

실무 화면 예

  • 주문 목록 정렬
  • 검색 결과 필터
  • 랭킹/추천 목록
  • React state 배열 처리

코드 리뷰 체크포인트

  • sort, reverse, splice처럼 원본을 바꾸는 메서드를 알고 쓰는가?
  • 외부에서 받은 배열을 직접 수정하지 않는가?
  • 정렬 결과와 원본 데이터의 책임이 분리되어 있는가?

변형 과제

  • 최신순과 오래된순을 버튼으로 전환한다.
  • 원본 배열의 첫 번째 항목이 정렬 후에도 유지되는지 테스트한다.

예제 3. 비동기 결과를 기다리지 않는 버그

연결 문제: https://nst21c.com/?task=fetchJson#labApp

흔한 실패 코드

function loadUsers() {
  const users = fetch("/api/users").then((res) => res.json());
  return users.length;
}

fetch()는 바로 사용자 배열을 주지 않는다. Promise를 반환한다. 그런데 Promise를 배열처럼 다루면 화면에는 빈 값이 보이거나 오류가 난다.

개선 코드

async function loadUsers() {
  const response = await fetch("/api/users");
  if (!response.ok) {
    throw new Error("사용자 목록을 불러오지 못했습니다.");
  }

  const users = await response.json();
  return users.length;
}

실무 화면 예

  • 사용자 목록
  • 상품 검색 결과
  • 대시보드 통계 카드
  • 자동완성 검색창

코드 리뷰 체크포인트

  • Promise를 실제 데이터처럼 바로 사용하지 않는가?
  • await가 필요한 위치에 있는가?
  • 실패 응답을 성공 데이터처럼 처리하지 않는가?

변형 과제

  • try/catch로 실패 메시지를 반환한다.
  • 빈 배열일 때 "결과 없음"을 반환한다.

예제 4. 이벤트 리스너 중복 등록 버그

연결 문제: https://nst21c.com/?task=domButton#labApp

흔한 실패 코드

function setupButton() {
  const button = document.querySelector("#save");
  button.addEventListener("click", save);
}

setupButton();
setupButton();

같은 이벤트가 두 번 등록되면 버튼을 한 번 눌러도 저장 요청이 두 번 나간다. 개발 중에는 잘 안 보이다가 실제 서비스에서 중복 주문, 중복 신청, 중복 저장으로 이어질 수 있다.

개선 코드

function setupButton() {
  const button = document.querySelector("#save");
  if (!button || button.dataset.bound === "true") return;

  button.dataset.bound = "true";
  button.addEventListener("click", save);
}

실무 화면 예

  • 저장 버튼
  • 신청 폼 제출
  • 모달 열기/닫기
  • 무한 스크롤 이벤트

코드 리뷰 체크포인트

  • 초기화 함수가 여러 번 실행될 가능성이 있는가?
  • 이벤트를 등록하기 전에 기존 등록 상태를 확인하는가?
  • 한 번만 실행되어야 하는 액션에 중복 방지가 있는가?

5일 복습 루틴

1일차: 숫자 문자열 계산 문제를 다시 풀고 입력값 출처를 적는다. 2일차: 배열 정렬 문제를 풀고 원본 변경 메서드를 정리한다. 3일차: fetch 문제를 풀고 Promise와 실제 데이터의 차이를 설명한다. 4일차: DOM 이벤트 문제를 풀고 중복 실행 방지 코드를 작성한다. 5일차: 네 예제를 하나의 “상품 목록 + 장바구니” 화면에 연결해본다.

이 팩 다음에 보낼 후속 안내

JavaScript 팩을 받은 사용자는 TypeScript로 넘어가기 전에 “값의 출처와 형태”를 계속 확인해야 한다. 후속 메일에서는 TypeScript의 unknown, optional 필드, API 응답 타입으로 연결한다.