PH pullh
언어 가이드 / JavaScript

LANGUAGE DOSSIER

JavaScript는 제품 실험이 빠른 팀에서 고객 반응을 가장 가까이서 다루는 언어다

웹 앱, 대시보드, 실험 페이지, 에지 함수. 사용자의 클릭과 팀의 실험 속도가 곧 성과인 조직에서 JavaScript는 여전히 가장 앞줄에 있다.

프런트엔드 제품팀그로스 실험 팀웹 대시보드 팀
JavaScript 커버 이미지
학습 예제 200
카테고리 12
잘 맞는 팀 제품 실험 팀

사용자 반응을 바로 보고 실험을 다시 거는 루프가 중요할 때 JavaScript는 제품 팀의 주력 도구가 된다.

한 개의 스레드 위에서 벌어지는 일

JavaScript를 어렵게 만드는 것은 문법이 아닙니다. 문법만 보면 몇 시간이면 훑습니다. 사람을 붙잡는 것은 실행 모델입니다. 코드는 한 줄씩 순서대로 적혀 있는데 실제로 실행되는 순서는 그 순서가 아니고, 그 차이를 만드는 규칙이 눈에 보이지 않습니다. 이벤트 루프, this, 클로저 세 가지에서 생기는 착시를 걷어내면 나머지는 대체로 손에 익는 일입니다.

JavaScript가 기본값인 자리, 그리고 아닌 자리

브라우저에서 무언가를 움직이게 하려면 선택지가 사실상 하나입니다. 화면을 조작하고, 입력을 받고, 네트워크를 호출하는 일은 결국 JavaScript로 내려옵니다. 다른 언어로 작성하더라도 마지막에는 JavaScript나 그것이 다루는 런타임 위에서 실행됩니다. 여기에 더해 같은 언어로 서버까지 다룰 수 있어서, 인원이 적은 팀은 프런트와 간단한 API와 자동화 스크립트를 한 사고방식으로 가져갈 수 있습니다. 대시보드, 랜딩 페이지, 실험용 위젯처럼 자주 바뀌고 짧게 사는 것들에 특히 잘 맞습니다.

반대로 억지로 쓰면 손해인 자리도 있습니다. CPU를 오래 붙잡는 계산은 단일 스레드 모델과 잘 맞지 않아서 별도 워커나 다른 언어로 넘기게 됩니다. 정밀한 소수 계산이 필요한 금융 계산은 기본 숫자 타입만으로는 위험하고, 메모리를 세밀하게 다뤄야 하는 시스템 영역이나 대규모 수치 연산도 이 언어의 강점이 아닙니다. 그리고 규모가 커진 코드베이스를 여러 명이 오래 고쳐야 한다면, 뒤에서 다룰 이유로 순수 JavaScript만으로 버티기는 점점 어려워집니다.

이벤트 루프를 이해하면 절반은 끝난다

JavaScript는 하나의 스레드에서 실행됩니다. 동시에 두 줄이 돌아가는 일은 없습니다. 대신 지금 실행할 것이 없을 때 대기열에서 다음 작업을 꺼내 오는 순환이 돌아갑니다. 이 구조 때문에 두 가지가 동시에 참입니다. 하나, 공유 변수에 대한 잠금을 걱정할 일이 거의 없습니다. 둘, 내 코드가 100밀리초 동안 계산을 붙잡으면 그동안 클릭도 화면 갱신도 전부 밀립니다. 프런트에서 화면이 뚝뚝 끊기는 문제의 상당수는 여기서 옵니다.

대기열이 하나가 아니라는 점도 알아둬야 합니다. setTimeout으로 예약한 콜백과 프로미스가 완료됐을 때 실행되는 콜백은 서로 다른 줄에 섭니다. 프로미스 쪽은 지금 실행 중인 코드가 끝나자마자, 다음 타이머 작업보다 먼저 처리됩니다. 그래서 지연 시간을 0으로 준 타이머보다 이미 완료된 프로미스의 then이 먼저 실행됩니다. 순서가 이상해 보이는 로그를 만나면 대개 이 구분을 모르고 있는 경우입니다. async와 await는 이 흐름을 동기 코드처럼 보이게 적어 주는 문법이지 스레드를 하나 더 만들어 주는 도구가 아닙니다. 언어별로 비동기를 어떻게 다루는지는 비동기 처리 비교에서, 실전 패턴은 JavaScript 비동기 가이드에서 볼 수 있습니다.

순서가 헷갈릴 때는 규칙을 외우기보다 한 문장으로 기억하는 편이 빠릅니다. 지금 실행 중인 코드는 끝까지 실행되고, 그다음 프로미스 콜백이 모두 처리되고, 그다음에야 타이머와 이벤트가 차례를 받습니다. 이 문장 하나로 설명되지 않는 로그를 만났다면 그때 자세히 파고들면 됩니다.

this와 클로저가 만드는 착시

다른 언어에서 온 사람이 가장 자주 데는 곳이 this입니다. 여기서 this는 함수가 어디에 정의됐는지가 아니라 어떻게 호출됐는지로 정해집니다. 객체의 메서드로 호출하면 그 객체를 가리키지만, 같은 함수를 변수에 담아 콜백으로 넘기면 호출 시점에 소속이 사라져 다른 것을 가리킵니다. 메서드를 이벤트 핸들러로 그대로 등록했을 때 갑자기 값이 없다고 나오는 상황이 이 경우입니다.

화살표 함수는 규칙이 다릅니다. 자기 this를 만들지 않고 정의된 자리의 것을 그대로 씁니다. 그래서 콜백 안에서 바깥 객체를 계속 가리키게 하고 싶을 때 편합니다. 반대로 객체 리터럴의 메서드를 화살표 함수로 적으면 그 객체를 가리키지 못합니다. 화살표 함수가 더 새로운 문법이니 항상 낫다고 배우면 이 지점에서 막힙니다.

클로저는 반대 방향의 착시입니다. 함수 안에서 바깥 변수를 쓰면 그 값을 복사해 두는 게 아니라 변수 자체를 계속 붙잡습니다. 반복문 안에서 콜백을 여러 개 만들고 나중에 실행했을 때 전부 마지막 값을 출력하는 고전적인 문제가 여기서 나옵니다. 반복마다 새 바인딩을 만드는 선언을 쓰면 대부분 해결되지만, 왜 해결되는지를 아는 것과 그냥 그렇게 쓰는 것은 나중에 차이가 납니다.

객체 모델도 다릅니다. class 문법이 있지만 그 아래는 프로토타입 연결입니다. 객체에서 속성을 찾지 못하면 연결된 다른 객체로 올라가면서 계속 찾습니다. 평소에는 몰라도 되지만, 라이브러리가 기존 객체를 확장해 두었거나 상속 구조가 이상하게 동작할 때는 이 그림을 알아야 원인이 보입니다. 값 비교에서 동등 연산자 대신 일치 연산자를 쓰라는 조언도 같은 맥락입니다. 느슨한 비교는 타입이 다르면 한쪽을 변환한 뒤 비교하는데, 그 변환 규칙은 외울 가치가 없을 만큼 복잡합니다. 항상 일치 연산자를 쓰고 필요한 변환은 직접 적는 편이 낫습니다.

타입이 없다는 것의 실제 비용

혼자 만드는 200줄짜리 스크립트에서 타입이 없는 것은 이득입니다. 적어야 할 것이 적고 고치는 속도가 빠릅니다. 비용은 코드가 커지고 사람이 늘어난 뒤에 청구됩니다. 함수가 무엇을 받는지 알려면 호출하는 쪽을 다 열어 봐야 하고, 응답 JSON의 필드 이름 하나가 바뀌면 실행해서 화면이 깨질 때까지 모릅니다. 오타는 문법 오류가 아니라 undefined로 조용히 흘러갑니다.

그래서 판단 기준을 미리 정해 두면 편합니다. 코드가 여러 파일로 나뉘고, 다른 사람이 내 함수를 부르고, 서버와 주고받는 데이터 구조가 있다면 TypeScript로 옮길 때가 된 것입니다. 반대로 한 파일짜리 실험 코드나 곧 지울 스크립트라면 굳이 얹을 필요가 없습니다. 옮긴다고 해서 다른 언어를 새로 배우는 것은 아닙니다. 실행 모델과 이벤트 루프와 this 규칙은 그대로이고, 타입 표기가 얹힐 뿐입니다. 순서로는 JavaScript의 실행 모델을 먼저 잡고 TypeScript 기초로 넘어가는 편이 안전합니다. 먼저 붙는 타입 때문에 정작 언어를 이해할 기회를 놓치는 경우가 생각보다 많습니다.

Why It Works

이럴 때 특히 힘을 발합니다

고객 반응과 가장 가깝다

브라우저 안에서 벌어지는 일들을 바로 제어하므로 제품 실험과 사용자 행동 관찰이 빠르다.

작은 기능을 자주 내기 좋다

A/B 테스트, 랜딩 페이지, 운영용 위젯처럼 짧은 주기로 움직이는 기능에 강하다.

웹 전체에 걸친 연결이 쉽다

프런트, 간단한 백엔드, 자동화 스크립트를 비슷한 감각으로 가져갈 수 있어 작은 팀에 유리하다.

Business Scenes

사업 현장에서 자주 보이는 장면

그로스 실험

배너, 폼, 결제 진입 버튼, 가입 퍼널을 빠르게 바꾸고 바로 데이터를 볼 수 있다.

실시간 대시보드

운영 지표, 알림, 현황판처럼 가시성이 중요한 화면을 빠르게 조립하기 좋다.

고객 접점 위젯

채팅, 피드백 수집, 예약 도우미 같은 웹 위젯은 JavaScript로 실험과 배포를 반복하기 쉽다.

Workflow

팀이 움직이는 방식

  1. 실험은 UI 한 조각보다 측정 이벤트 이름부터 정한다.
  2. 브라우저에서 필요한 계산과 서버에서 해야 할 계산을 구분한다.
  3. 제품 회고는 코드보다 퍼널 숫자와 클릭 맥락을 먼저 본다.
  4. 짧은 실험일수록 제거 비용이 낮은 구조로 짠다.

그로스 팀이 자주 보는 형태

const cohort = users
  .filter((user) => user.signupPath === 'campaign-a')
  .map((user) => ({
    id: user.id,
    converted: user.orders.length > 0,
  }));

console.log(cohort);

실험 코드는 멋진 추상화보다 “어떤 사용자군을 보고 있는지”가 바로 읽히는 편이 좋다.

위 코드가 저 형태인 이유

앞의 코호트 코드는 반복문을 쓰지 않습니다. filter로 대상을 좁히고 map으로 필요한 모양만 남기는 순서가 그대로 읽히기 때문입니다. 원본 배열을 고치지 않고 새 배열을 만든다는 점도 중요합니다. 실험 코드는 여러 사람이 콘솔에서 잘라 붙여 가며 확인하는 일이 많은데, 중간에 원본이 바뀌어 있으면 같은 코드를 두 번 돌렸을 때 결과가 달라집니다. 조건을 변수 이름으로 설명하는 대신 값 자체로 드러내는 편이 실험에서는 유리합니다.

다만 실제 데이터는 이미 배열로 손에 들려 있지 않습니다. 어딘가에서 받아 와야 하고, 그 과정에서 실패할 수 있습니다.

데이터를 받아 오는 쪽까지 포함한 형태

async function loadCohort(campaign, signal) {
  const url = `/api/users?campaign=${encodeURIComponent(campaign)}`;
  const res = await fetch(url, { signal });

  if (!res.ok) {
    throw new Error(`코호트 조회 실패: ${res.status}`);
  }

  const users = await res.json();

  return users.map((user) => ({
    id: user.id,
    converted: (user.orders?.length ?? 0) > 0,
  }));
}

await는 이 함수만 멈춰 세울 뿐, 브라우저 전체를 멈추지 않습니다.

몇 가지가 의도적입니다. fetch는 서버가 오류 상태 코드를 돌려줘도 실패로 처리하지 않기 때문에 응답이 정상인지 직접 확인해야 합니다. 이 한 줄이 없으면 오류 페이지의 HTML을 JSON으로 읽으려다 엉뚱한 자리에서 터집니다. 취소 신호를 인자로 받는 것은 화면이 사라진 뒤 도착한 응답이 이미 없는 요소를 건드리는 상황을 막기 위해서입니다. 그리고 값이 없을 수 있는 자리에는 옵셔널 체이닝과 기본값을 붙였습니다. 타입이 없는 언어에서 undefined는 오류가 아니라 조용한 값이므로, 경계에서 한 번 막아 두는 편이 낫습니다.

도구 체인은 언어보다 자주 바뀐다

이 생태계에서 오래 남는 것은 언어이고, 자주 바뀌는 것은 그 주변입니다. 몇 년 전 튜토리얼이 지금 그대로 통하지 않는 이유도 대부분 언어가 아니라 도구 때문입니다. 그래서 특정 도구의 설정 파일을 외우는 대신, 각 도구가 어떤 문제를 대신 풀고 있는지를 기억하는 편이 유효 기간이 깁니다.

패키지 관리자는 의존성을 내려받고 버전을 고정합니다. npm이든 pnpm이든 잠금 파일을 저장소에 함께 커밋한다는 원칙은 같습니다. 번들러는 여러 파일로 나뉜 코드를 브라우저가 받기 좋은 형태로 묶고, 개발 중에는 바뀐 부분만 즉시 반영해 줍니다. 여기서 모듈 형식 문제가 자주 튀어나옵니다. 브라우저와 최신 코드가 쓰는 방식과 오래된 Node 패키지가 쓰는 방식이 다르고, 둘이 섞인 프로젝트에서 나는 오류 메시지는 대체로 불친절합니다. 처음 보는 import 관련 오류의 상당수가 이 경계입니다.

포매터와 린터는 성격이 다릅니다. 하나는 스타일을 통일해 코드 리뷰에서 줄바꿈 이야기를 없애고, 다른 하나는 문법은 맞지만 문제가 될 만한 패턴을 잡습니다. 둘 다 저장 시점과 CI 양쪽에 걸어 두면 논쟁이 줄어듭니다. 테스트 러너는 번들러 설정을 공유하는 쪽을 고르면 설정이 두 벌로 갈라지지 않습니다. 무엇을 어떻게 테스트할지는 JavaScript 테스트 가이드에 정리해 두었습니다.

실행 대상이 둘이라는 점도 계속 따라다닙니다. 같은 언어지만 브라우저에는 DOM과 화면 관련 API가 있고 파일 시스템이 없으며, Node에는 그 반대가 있습니다. 브라우저 코드에서 서버용 모듈을 불러오면 번들 단계나 실행 시점에 깨집니다. 어느 쪽을 목표로 하는 코드인지 파일 단위로 분명히 나눠 두는 습관이 사고를 줄입니다.

문제를 볼 때는 브라우저 개발자 도구가 가장 빠른 도구입니다. 번들된 코드를 원본과 대응시켜 주는 소스 맵을 켜 두면 스택 트레이스가 실제 파일과 줄 번호를 가리킵니다. 화면이 끊긴다면 성능 탭에서 기록을 떠서 어떤 작업이 스레드를 오래 잡고 있는지 봅니다. 추측 대신 기록을 먼저 보는 순서는 JavaScript 성능 가이드에서도 같습니다.

지금 깊게 팔 가치가 있는가

  • 웹 화면을 만드는 일이 업무에 조금이라도 포함된다면 선택의 문제가 아닙니다. 프레임워크를 쓰더라도 결국 이 언어의 규칙 위에서 디버깅하게 됩니다.
  • 제품 실험이나 데이터 시각화처럼 사용자 반응을 직접 보는 일을 한다면, 화면과 이벤트를 손에 쥘 수 있다는 점이 그대로 속도가 됩니다.
  • 혼자 또는 두세 명이 서비스 전체를 만들어야 한다면, 프런트와 간단한 서버를 한 언어로 가져가는 이점이 큽니다.
  • 지금은 아니다: 데이터 분석이나 머신러닝이 목표라면 도구와 자료가 모여 있는 쪽에서 시작하는 편이 훨씬 빠릅니다.
  • 지금은 아니다: 무거운 계산이나 메모리를 세밀하게 다루는 일이 중심이라면 단일 스레드 모델과 계속 싸우게 됩니다.
  • 지금은 아니다: 유행하는 프레임워크 이름부터 고르고 시작하려 한다면 잠시 미루세요. 이벤트 루프와 this와 클로저를 모른 채 프레임워크에 올라타면, 오류 메시지가 프레임워크 안쪽을 가리키는 순간 멈춰 섭니다.

실험 로그

가입 전환율 3%를 움직인 JavaScript 실험 로그

버튼 색을 바꾼 이야기가 아니다. 카피, 이벤트, 대기 시간을 같이 만지며 제품 팀이 어떻게 학습했는지 적었다.