[TYPESCRIPT] DOM 타입 정의 이해하기 — TypeScript로 안전한 브라우저 코드 작성

 

프론트엔드 개발에서 DOM(Document Object Model)을 다루는 코드는 매우 흔하지만, JavaScript만 사용할 경우 null 접근, 잘못된 캐스팅, 런타임 오류가 쉽게 발생합니다. TypeScript는 DOM 타입 정의(lib.dom.d.ts)를 통해 이러한 문제를 컴파일 단계에서 미리 차단합니다.

 

 

DOM 타입 정의는 어디서 오는가?

TypeScript는 기본적으로 lib.dom.d.ts라는 타입 정의 파일을 통해 브라우저 환경의 API를 타입으로 제공합니다.

  • Document
  • HTMLElement
  • HTMLDivElement
  • Event, MouseEvent, KeyboardEvent

이 파일은 TypeScript에 기본 포함되어 있으며, tsconfig.jsonlib 옵션에 따라 로딩됩니다.

{
  "compilerOptions": {
    "lib": ["DOM", "ES2020"]
  }
}

브라우저용 프로젝트라면 대부분 DOM 라이브러리를 포함해야 합니다.

 

document와 HTMLElement 기본 타입

document 객체는 Document 타입으로 정의되어 있습니다.

const el = document.getElementById("app");

이때 el의 타입은 다음과 같이 추론됩니다.

HTMLElement | null

DOM API는 요소가 없을 수 있기 때문에 항상 null 가능성을 포함합니다. 이 점이 TypeScript가 DOM 코드를 까다롭게 만드는 핵심 이유입니다.

 

null 체크는 필수

다음 코드는 컴파일 에러가 발생합니다.

const el = document.getElementById("app");
el.innerHTML = "Hello"; // X Object is possibly 'null'

안전한 접근 방식은 명시적인 null 체크입니다.

const el = document.getElementById("app");
if (el) {
  el.innerHTML = "Hello";
}

이 패턴은 DOM 접근 시 가장 기본적인 규칙입니다.

 

querySelector와 제네릭

querySelector는 제네릭을 통해 구체적인 요소 타입을 지정할 수 있습니다.

const input = document.querySelector<HTMLInputElement>("#email");

이 경우 타입은 다음과 같습니다.

HTMLInputElement | null

덕분에 다음과 같은 코드가 안전해집니다.

if (input) {
  input.value = "test@example.com";
}

실무에서는 querySelector + 제네릭 패턴이 가장 권장됩니다.

 

HTMLElement vs HTMLInputElement 차이

DOM 타입은 상속 구조를 가집니다.

  • Node
  • Element
  • HTMLElement
  • HTMLInputElement / HTMLDivElement / HTMLButtonElement …

따라서 HTMLElement에는 없는 속성이 하위 타입에는 존재합니다.

const el = document.querySelector("input");
el?.value; // X HTMLElement에는 value가 없음

정확한 타입을 지정해야만 올바른 속성 접근이 가능합니다.

 

타입 단언(as) 사용은 최후의 수단

다음과 같이 타입 단언을 사용할 수도 있습니다.

const input = document.getElementById("email") as HTMLInputElement;
input.value = "test";

하지만 이 방식은 컴파일러의 안전 장치를 우회합니다.

  • null 가능성 무시
  • 실제 DOM 구조 변경 시 런타임 오류 가능

따라서 querySelector 제네릭 + null 체크를 우선 고려해야 합니다.

 

이벤트(Event) 타입 이해하기

DOM 이벤트 역시 모두 타입으로 정의되어 있습니다.

const button = document.querySelector("button");

button?.addEventListener("click", (event) => {
  event.preventDefault();
});

이때 event의 타입은 MouseEvent로 추론됩니다.

이벤트 종류에 따라 타입이 다릅니다.

  • click → MouseEvent
  • keydown → KeyboardEvent
  • input → Event / InputEvent

 

event.target과 currentTarget의 차이

DOM 이벤트에서 자주 헷갈리는 부분입니다.

button?.addEventListener("click", (event) => {
  const target = event.target;
  const current = event.currentTarget;
});
  • target → 실제 이벤트가 발생한 요소 (타입이 좁지 않음)
  • currentTarget → 리스너가 바인딩된 요소

TypeScript에서는 currentTarget이 더 안전합니다.

button?.addEventListener("click", (event) => {
  const btn = event.currentTarget as HTMLButtonElement;
  btn.disabled = true;
});

 

실무에서 자주 쓰는 안전 패턴

  • querySelector + 제네릭
  • 명시적 null 체크
  • HTMLElement 대신 구체적인 Element 타입 사용
  • event.currentTarget 활용
  • as 단언은 최소화

이 패턴만 지켜도 DOM 관련 버그의 상당수를 예방할 수 있습니다.

 


 

DOM 타입 정의는 “브라우저 API를 안전하게 사용하기 위한 TypeScript의 보호막”입니다. 처음에는 다소 번거롭게 느껴질 수 있지만, 한 번 익숙해지면 런타임 오류를 크게 줄일 수 있습니다.

  • DOM API는 기본적으로 null 가능
  • 구체적인 Element 타입 지정이 핵심
  • 타입 단언보다 타입 추론과 체크 우선