네이버 이웃 추가 / GitHub Profile / 카카오톡 채널 추가 / 방명록 / 이용 안내

TypeScript DOM

수성컴 | 2024. 11. 16.
TypeScript DOM

수성컴전자방입니다. JavaScript로 HTML의 요소를 수정할 때는 HTML DOM을 이용합니다. 여러분이 보고 계신 이 블로그의 왼쪽 상단 메뉴 버튼도 HTML DOM으로 구현되었죠. 오늘은 TypeScript에서 HTML DOM을 사용할 때 타입 관련 부분을 어떻게 처리해야 하는지 알아보겠습니다. HTML DOM을 처리하는 메소드는 getElementById, querySelector 등 다양하지만 오늘은 getElementById로 설명 드리겠습니다.(어차피 TypeScript 관련 설명은 똑같음.)

목차

1. 예제 폴더 구조 및 public/index.html, tsconfig.json
1.1. index.html
1.2. tsconfig.json

2. 문제 발생
3. !=null로 narrowing
4. Optional chaining(?.)으로 narrowing
5. instanceof 연산자로 narrowing
6. 글 마무리
7. 참고 자료

1. 예제 폴더 구조 및 public/index.html, tsconfig.json

저는 아래와 같은 폴더 구조로 진행하겠습니다.(굵은 글씨는 폴더)

  • public: HTML 파일과 컴파일 결과 생성된 JavaScript 파일이 들어가는 폴더
    • index.html: 1.1번 문단
  • src: TypeScript 소스 코드가 들어가는 폴더
    • index.ts: 오늘의 예제 코드★(2~4번 문단)
  • tsconfig.json: 1.2번 문단

1.1. index.html

<!DOCTYPE html>
<html>

<head>
<title>JS 모듈 테스트</title>
<script defer src="index.js"></script>
</head>

<body>

<header>
	<h1>JavaScript 모듈 테스트</h1>
</header>

<section>
    <input type="text" id="a"/><input type="text" id="b"/><br>
    더하기: <span id="result_add"></span><br>
    <button id="add" type="button">더하기</button>
</section>

</body>
</html> 

1.2. tsconfig.json

tsconfig.json은 중요한 것만 보여드립니다.

{
  "compilerOptions": {
    //...

    /* Emit */
    "outDir": "public",                                   /* Specify an output folder for all emitted files. */
    
    //...

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */

    //...
  },

  "include": ["src"]
}

이번 글에서 target, module은 임의로 설정하시면 됩니다.

2. 문제 발생

function runadd(): void{
    let element_a=document.getElementById("a");
    let element_b=document.getElementById("b");
    let element_result=document.getElementById("result_add");

    let a: string=element_a.value;
    let b: string=element_b.value;
 
    let num1: number=Number(a);
    let num2: number=Number(b);

    if (isNaN(num1) || isNaN(num2)) {
        alert("숫자만 입력하세요!");
        return;
    }
    
    element_result.innerHTML=String(num1+num2);
}

let element_btn=document.getElementById("add");
element_btn.addEventListener("click", runadd);

대충 JavaScript 할 때처럼 코드를 작성해 보았습니다. 그런데…

possibly null
tsc 명령으로 컴파일해 보면 오류가 뜨는 것을 보실 수 있습니다. 우선 위의 스크린샷에서 제가 분홍색으로 표시한 부분을 보겠습니다.
객체명’ is possibly ‘null’.이라고 합니다.
document.getElementById의 type이 HTMLElement|null로 Union type이기 때문에 발생하는 문제입니다. 20행을 예로 들어 설명 드리면 id가 “add”인 요소를 불러오고자 하는데 id가 “add”인 요소가 없으면 null이 되기 때문에 그 다음 21행 작업을 할 수 없다는 것입니다.
이 문제를 해결하기 위해 narrowing을 해야 합니다.

3. !=null로 narrowing

function runadd(): void{
    let element_a=document.getElementById("a");
    let element_b=document.getElementById("b");
    let element_result=document.getElementById("result_add");

    let a: string="0";
    let b: string="0";

    if(element_a!=null){
        a=element_a.value;
    }
    
    if(element_b!=null){
        b=element_b.value;
    }
    
    let num1: number=Number(a);
    let num2: number=Number(b);

    if (isNaN(num1) || isNaN(num2)) {
        alert("숫자만 입력하세요!");
        return;
    }
    
    if(element_result!=null){
        element_result.innerHTML=String(num1+num2);
    }
}

let element_btn=document.getElementById("add");
if(element_btn!=null){
    element_btn.addEventListener("click", runadd);
}

문제가 발생하는 코드(2번 문단)와 비교했을 때 6~15, 25~27, 31~33행이 달라진 부분입니다. 조건문을 이용합니다. 가져온 요소가 null이 아닐 경우에만 동작하게 합니다. element_result를 예로 들겠습니다.

[Line 4]
let element_result=document.getElementById(“result_add”);
id가 result_add인 element를 element_result에 불러옵니다.

[Line 25]
if(element_result!=null)
이렇게 쓰시면 element_result이 null이 아닐 때만 중괄호 안의 내용(Line 26)이 실행됩니다.

error TS2339: Property 'value' does not exist on type 'HTMLElement'.
tsc 명령으로 컴파일해 보면 오류가 많이 줄어든 것을 보실 수 있습니다. 일단 2번 문단에서 제가 분홍색으로 표시한 오류는 모두 해결되었습니다. 그러나 input 태그의 value가 HTMLElement에 존재하지 않는다는 오류 메시지가 여전히 뜹니다.

4. Optional chaining(?.)으로 narrowing

function runadd(): void{
    let element_a=document.getElementById("a");
    let element_b=document.getElementById("b");
    let element_result=document.getElementById("result_add");

    let a: string=element_a?.value;
    let b: string=element_b?.value;
 
    let num1: number=Number(a);
    let num2: number=Number(b);

    if (isNaN(num1) || isNaN(num2)) {
        alert("숫자만 입력하세요!");
        return;
    }
    
    if(element_result?.innerHTML!=undefined){
        element_result.innerHTML=String(num1+num2);
    }
}

let element_btn=document.getElementById("add");
element_btn?.addEventListener("click", runadd);

문제가 발생하는 코드(2번 문단)와 비교했을 때 6~7, 17~19, 23행이 달라진 부분입니다. Optional chaining은 JavaScript에서 지원하는 문법으로, 객체의 속성에 접근할 때 . 대신 ?.을 쓰면 객체가 null일 때 표현 결과 값이 undefined로 나오게 하는 문법입니다. element_result를 예로 들겠습니다.

[Line 4]
let element_result=document.getElementById(“result_add”);
id가 result_add인 element를 element_result에 불러옵니다.

[Line 17]
if(element_result?.innerHTML!=undefined)
Optional chaining은 대입연산자(=) 왼쪽에 사용할 수 없습니다. 따라서 17~19행은 6~7, 23행과 달리 조건문을 사용해 줍니다. element_result이 null이 아닐 때 element_result?.innerHTML!=undefined가 성립하며 중괄호 안(Line 18)의 내용이 실행됩니다.
Optional chaining이 대입연산자(=) 오른쪽에 있는 6~7, 23행은 조건문 없이 가능합니다.
23행) element_btn?.addEventListener(“click”, runadd);

error TS2339: Property 'value' does not exist on type 'HTMLElement'.
tsc 명령으로 컴파일해 보면 3번 문단과 마찬가지로 2번 문단에서 제가 분홍색으로 표시한 오류는 모두 해결되었습니다. 그러나 input 태그의 value가 HTMLElement에 존재하지 않는다는 오류 메시지가 여전히 뜹니다.

5. instanceof 연산자로 narrowing

function runadd(): void{
    let element_a=document.getElementById("a");
    let element_b=document.getElementById("b");
    let element_result=document.getElementById("result_add");

    let a: string="0";
    let b: string="0";

    if(element_a instanceof HTMLInputElement){
        a=element_a.value;
    }
    
    if(element_b instanceof HTMLInputElement){
        b=element_b.value;
    }
    
    let num1: number=Number(a);
    let num2: number=Number(b);

    if (isNaN(num1) || isNaN(num2)) {
        alert("숫자만 입력하세요!");
        return;
    }
    
    if(element_result instanceof HTMLElement){
        element_result.innerHTML=String(num1+num2);
    }
}

let element_btn=document.getElementById("add");
if(element_btn instanceof HTMLElement){
    element_btn.addEventListener("click", runadd);
}

문제가 발생하는 코드(2번 문단)와 비교했을 때 6~15, 25~27, 31~33행이 달라진 부분입니다. 조건문을 이용하되, != 대신 instanceof를 사용합니다. element_a와 element_result를 예로 들겠습니다.

[Line 2, 4]
2행) let element_a=document.getElementById(“a”);
4행) let element_result=document.getElementById(“result_add”);

[Line 9, 25]
9행) if(element_a instanceof HTMLInputElement)
25행) if(element_result instanceof HTMLElement)
9행은 element_aHTMLInputElement의 객체일 때 중괄호 안(Line 10)의 내용을 실행하고, 25행은 element_resultHTMLElement의 객체일 때 중괄호 안(Line 26)의 내용을 실행합니다.
HTMLElement는 모든 HTML 태그에 적용 가능합니다.
HTMLElement를 상속받는 인터페이스로는 HTMLAnchorElement(<a> 태그 속성에 접근할 때 사용), HTMLAudioElement, HTMLButtonElement, HTMLImageElement, HTMLInputElement, HTMLVideoElement 등이 있습니다.

오류 없이 깔끔한 터미널
tsc 명령으로 컴파일해 보면 오류가 발생하지 않는 것을 보실 수 있습니다.
이것은 9, 13행에서 instanceof HTMLInputElement를 사용했고, 따라서 해당 객체가 HTMLInputElement의 객체임을 확인하여 value 속성을 사용할 수 있게 되었기 때문입니다.

정상 작동
index.html을 웹 브라우저로 실행하여 수를 입력하고 버튼을 클릭하니 정상적으로 작동하였습니다.

6. 글 마무리

제 생각에는 HTMLElement에 있는 속성값을 사용할 때는 간단하게 ?.를, 특정 태그에 있는 속성값을 사용할 때는 instanceof를 사용하는 것이 좋겠다는 생각이 듭니다.

참고로

if(element_a instanceof HTMLInputElement){
        a=element_a.value;
}

이 코드는

if(element_a instanceof HTMLInputElement)   a=element_a.value;

이렇게 한 줄로 나타낼 수도 있습니다.

제 글을 읽어 주셔서 감사합니다. 다음에 만나요!

7. 참고 자료

1) 코딩애플. 2021. “TypeScript로 웹개발하려면 HTML 조지는법을 알아야”, YouTube. (2024. 11. 16. 방문). https://youtu.be/iZjfnoF784k?si=MvqSqAnkNdkTwGg7
2) mdn web docs. 2024. “The HTML DOM API”, mdn web docs. (2024. 11. 16. 방문). https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API
3) 왜 안되지. 2022. “HTML Dom narrowing”, 이게 왜 되지. (2024. 11. 16. 방문). https://whyworks.tistory.com/26
4) DeveloperDH. 2024. “[TypeScript] JavaScript 슈팅게임에 TypeScript 적용하기! #5 - DOM 조작에 대한 Type Narrowing”, DH의 개발 공부로그. (2024. 11. 16. 방문). https://shape-coding.tistory.com/entry/TypeScript-JavaScript-슈팅게임에-TypeScript-적용하기-5-DOM-조작에-대한-Type-Narrowing