본문 바로가기
source-code/TypeScript

getElementsByClassName에 for each 사용해 HTMLElement 접근하기

by mattew4483 2023. 11. 16.
728x90
반응형

같은 className의 요소들을 순회하며 특정 동작을 수행해야 할 때가 있다.

  const balls = document.getElementsByClassName("ball");
  balls.forEach((element) => {}); // Error!

위와 같이 작성하면 될 것 같지만...

HTMLCollectionOf 에는 forEach가 없단다. 왜?

 

https://developer.mozilla.org/ko/docs/Web/API/HTMLCollection

 

HTMLCollection - Web API | MDN

HTMLCollection 인터페이스는 요소의 문서 내 순서대로 정렬된 일반 컬렉션(arguments처럼 배열과 유사한 객체)을 나타내며 리스트에서 선택할 때 필요한 메서드와 속성을 제공합니다.

developer.mozilla.org

당연히 없으니까 없다.

HTMLCollection은 요소의 문서 내 순서대로 정렬된 유사 배열이며,

특정 요소를 선택하는데 필요한 메서드을 별로도 제공하고 있다.

  const balls = document.getElementsByClassName("ball");
  balls.length; // number
  balls[1]; // Element
  balls.item(1); // Element

즉 위와 같은 속성에는 접근 가능하지만,

배열에만 존재하는 forEach, map, filter... 등의 메서드는 사용할 수 없다.

 

그렇다면 className으로 조회한 요소들을 순회하기 위해서는 어떻게 해야 할까?

 

1. for 문으로 순회

  const balls = document.getElementsByClassName("ball");
  for (let i = 0; i <= balls.length; i++) {
    balls[i];
  }

2. Array로 변환한 후 순회

 const balls = document.getElementsByClassName("ball");
  Array.from(balls).forEach((ball) => {
    
  });

 

 

위 두 방법 다 정상적으로 동작하고, 대부분 위의 방법을 추천하는데...

여기서 문제는? getElementsByClassName이 반환하는 값이 타입이 HTMLCollectionOf <Element>라는 것!

 

위 상황에서 class name을 통해 각 요소들을 조회한 이유는

대부분 각 html 요소의 속성에 접근하기 위함일 테다! (가장 대표적으로 style이 있겠다)

 

하지만 해당 속성들은 대부분 Element를 상속받은 HtmlElement 타입에 정의되어 있기 때문에...

  const balls = document.getElementsByClassName("ball");
  Array.from(balls).forEach((ball) => {
    ball.style.backgroun = 'red' // Error!
  });

위 코드는 다음과 같은 에러가 발생한다.

(1번, 2번 방법 모두 마찬가지!)

  const balls = document.getElementsByClassName(
    "ball"
  ) as unknown as HTMLElement[];
  Array.from(balls).forEach((ball) => {
    ball.style.background = "red";
  });

물론 위와 같이 강제 타입 형변환을 해도 동작은 하지만,

타입을 강제로 정의한다는 점에서... 만족스럽다고 볼 수는 없다. (as 쓸 거면 TS 쓰지 말자)

 

그렇다면 어쩌면 좋을까?

3. querySelectorAll을 통한 제네릭 지정

querySelectorAll을 사용하면 된다.

  const balls = document.querySelectorAll(".ball");

엥? getElementsByClassName와 사용하는 방식만 다른 것 아니었나??

반환값부터 아예 다른 걸 확인할 수 있다.

 

https://developer.mozilla.org/ko/docs/Web/API/NodeList

 

NodeList - Web API | MDN

NodeList 객체는 일반적으로 element.childNodes와 같은 속성(property)과 document.querySelectorAll 와 같은 메서드에 의해 반환되는 노드의 콜렉션입니다.

developer.mozilla.org

NodeList 역시 HTMLCollection과 마찬가지로 유사 배열이지만,

구형 브라우저를 제외하고는 forEach 메서드를 통한 순회가 가능하다!

  const balls = document.querySelectorAll(".ball");
  balls.forEach((ball) => {
    ball.style.background = "red"; // Error!
  });

즉 위와 같이 작성해 줄 수 있는데...

여전히 동일한 오류가 발생한다. 당연!

 

하지만... 아까 querySelectorAll의 타입을 유심히 봤다면, 해결책을 금방 찾을 수 있다.

getElementsByClassName과 달리, 제네릭을 넘겨받는 모습!

querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

실제 타입 구현은 다음과 같다.

 

아하! 따라서

  const balls = document.querySelectorAll<HTMLElement>(".ball");
  balls.forEach((ball) => {
    ball.style.background = "red";
  });

다음과 같이, HTMLElement 인터페이스를 제네릭으로 넘겨주면

배열 속 요소를 HTMLElement로 추론할 수 있게 된다!

728x90
반응형