본문 바로가기

공부/트러블 슈팅

[트러블 슈팅] 영화 검색 사이트 (TypeError, 애니메이션, 이미지 로딩)

반응형

 

 

[팀 프로젝트] 영화 검색 사이트

팀 프로젝트 소개 📖TMDB API를 이용한 영화 검색 사이트입니다.Github 주소 : https://github.com/smu06030/movie-app-team12배포 링크 : http://movie-app-team12.s3-website.ap-northeast-2.amazonaws.com/  요구 사항 📌필수 구

mingos-habitat.tistory.com

영화 검색 사이트 개발을 진행하면서 발생한 트러블 슈팅에 대해 정리해 봤습니다.

 

1. TypeError: Cannot read properties of null (reading 'addEventListener')

발생한 오류 🔥

`common.js`에서 사용하는 formatMovie 메서드를 `detailPage.js`에서도 중복으로 사용하는 부분이 있어서 formatMovie 메서드를 export 하고 `detailPage.js`에서 import 하는 방식으로 사용했는데 TypeError: Cannot read properties of null (reading 'addEventListener') 에러가 발생했습니다.

const formatMovie = (movie) => ({
  id: movie.id,
  enTitle: movie.original_title.toLowerCase(),
  koTitle: movie.title,
  imgUrl: movie.poster_path,
  overview: movie.overview,
  rating: movie.vote_average.toFixed(2),
  date: movie.release_date.slice(0, 4),
});

 

해결 과정 🔎

자바스크립트 파일들 간에 의존 관계 때문에 하나의 js 파일만 사용하게 되면 기능별 구조 파악이 어렵고 복잡해서 버그가 생긴 것 같은 생각이 들었습니다.

 

해결 방법 ✨

별도 formatMovie.js라는 파일을 만들어서 각각 import 해주는 방식으로 문제를 해결했습니다.

formatMovie.js

 

 

2. 스켈레톤 UI 백그라운드 애니메이션 렉 걸리는 현상 발생

발생한 오류 🔥

스켈레톤 UI를 사용할 때 background-position 위치를 이용해 백그라운드 애니메이션을 구현했는데 화면이 렉걸리는 현상이 발생했습니다.

 

해결 과정 🔎

개발자 도구에서 요소 검사로 해당 UI가 어떻게 움직이는지 확인을 해 본 결과 `background-position` 속성을 사용하게 되면 배경 이미지의 위치를 조정하기 때문에 렌더링 과정에서 페인트 단계를 다시 수행하는 문제가 있었습니다. 결국 성능 저하 문제가 생길 수 있습니다.

 

개발자도구 -> performance -> Event Log 

`background-position`을 사용했을 때 paint 단계를 계속 수행하는걸 확인하실 수 있습니다.

background-position 속성 사용 시

 

 

`transform`을 사용했을 땐 paint 단계를 로딩 시 스켈레톤 UI를 띄울 때 수행하고 이후에는 paint 단계를 수행하지 않는 걸 볼 수 있습니다.

transform 속성 사용

 

해결 방법 ✨

`transform` 속성은 시각적 위치만 바뀌고 실제 DOM 위치는 변경되지 않기 때문에 레이아웃과 페인트 단계를 건너뛸 수 있어서 성능적인 측면에서 매우 효율적입니다.

.skeleton::after{
  transform: translateX(-100%);
  animation: load 1s infinite;
}

@keyframes load {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

 

DOM 위치는 그대로
시각적 요소만 변경

 

 

3. 이미지 로딩 속도

발생한 오류 🔥

로딩이 완료된 후에 이미지를 한 번에 보여주기 위해 스켈레톤 UI를 사용했는데 UI가 사라지고 난 후 이미지가 한 번에 보이는 게 아니라 각각 이미지 로딩이 다르게 보여서 사용자 경험 측면에서 불편한 느낌이 들었습니다. 

 

해결 과정 🔎

처음엔 아래와 같은 방식으로 진행했었는데 이미지 로딩 속도는 다 다르게 보였습니다.

// 데이터가 로드되면 스켈레톤 UI 보임
skeletons.style.display = 'flex';
movieDetails.style.display = 'none';

// 데이터가 로드되면 스켈레톤 UI 숨김
skeletons.style.display = 'none';
movieDetails.style.display = 'flex';

 

여기서 문제 스켈레톤 UI만 숨길뿐이지 이미지가 로딩이 됐는지 확인할 수 있는 방법이 없었습니다.

 

그래서 이미지가 로딩이 됐는지 확인할 수 있는 함수를 따로 만들었습니다.

 

해결 방법 ✨

`img` 모든 태그를 가져와 로딩이 완료가 됐는지 태그 요소를 하나씩 접근해 완료가 이미 되어 있으면 `imageLoadHandler` 메서드를 호출하고 아니라면 load, error 이벤트가 생겼을 때 콜백함수로 `imageLoadHandler`를 실행하게 작성했습니다.

images.forEach((image) => {
  if (image.complete) {
    imageLoadHandler();
  } else {
    image.addEventListener("load", imageLoadHandler);
    image.addEventListener("error", imageLoadHandler);
  }
});

 

 

`imageLoadHandler` 메서드에서 로딩, 실패한 이미지의 개수를 전체 이미지 개수와 비교해 모든 이미지가 로딩이 됐다면 `callback` 함수를 실행하게 코드를 작성했습니다.

 

const imageLoadHandler = (event) => {
if (!event) {
  loadedCount++;
} else {
  if (event.type === "load") {
    loadedCount++;
  } else {
    failedCount++;
  }
}

if (loadedCount + failedCount === totalImages) {
  callback();
}
};

 

 

모든 데이터가 로딩 됐다면 아래 스켈레톤 UI를 숨겨주는 콜백함수를 실행시키도록 작성했습니다.

checkImagesLoaded(() => {
  // 데이터가 로드되면 스켈레톤 UI 숨김
  selectors.skeletonDetail.style.display = "none";
  selectors.movieDetails.style.display = "flex";
      
  selectors.skeletonActor.style.display = "none";
  selectors.actorContainer.style.display = "flex";  
});

 

 

전체 코드입니다.

checkImagesLoaded(() => {
  // 데이터가 로드되면 스켈레톤 UI 숨김
  selectors.skeletonDetail.style.display = "none";
  selectors.movieDetails.style.display = "flex";

  selectors.skeletonActor.style.display = "none";
  selectors.actorContainer.style.display = "flex";  
});


// 모든 이미지 로드 완료 확인
const checkImagesLoaded = (callback) => {
  const images = document.querySelectorAll("img");

  let loadedCount = 0;
  let failedCount = 0;

  const totalImages = images.length;

  const imageLoadHandler = (event) => {
    if (!event) {
      loadedCount++;
    } else {
      if (event.type === "load") {
        loadedCount++;
      } else {
        failedCount++;
      }
    }

    if (loadedCount + failedCount === totalImages) {
      callback();
    }
  };

  images.forEach((image) => {
    if (image.complete) {
      imageLoadHandler();
    } else {
      image.addEventListener("load", imageLoadHandler);
      image.addEventListener("error", imageLoadHandler);
    }
  });
};

 

 

모든 이미지 로딩

 

 

깨달은 점 ❕

1. background-position vs transform: translate 차이점

`transform`은 시각적 위치만 바뀌고 실제 DOM 위치는 변경되지 않기 때문에 레이아웃과 페인트 단계를 건너뛸 수 있어서 성능적인 측면에서 매우 효율적입니다.


`background-position`은 배경 이미지의 위치를 조정하기 때문에 페인트 단계를 다시 수행해야 해서 성능 저하 문제가 생길 수 있습니다.

 

회고 🧐

코드를 작성할 때 렌더링 측면에 대해서 크게 생각해 본 적이 없었던 것 같은데 이번 프로젝트를 계기로 사용자 경험 측면에 대해서도 생각할 수 있어서 좋은 경험이었습니다!

 

 

 

반응형