Web

영화 검색 사이트 + 성능 최적화 (useCallback, useMemo, React.memo)

sungjae0309 2026. 6. 5. 02:27

TMDB API를 활용해 영화 검색 앱을 만들면서 검색 기능, 상세 모달, 언어 선택 기능을 구현하고, 불필요한 리렌더링을 줄이기 위해 useCallback, useMemo, React.memo를 직접 적용해보았다.

 

최종 실행 화면부터 보여주자면 아래와 같다. 


구현 

처음 앱에 진입했을 때 검색 결과가 비어 있으면 화면이 어색하게 느껴질 수 있다.

  • 따라서 기본적으로 인기 영화를 자동으로 불러오도록 구현했다.
  • 이를 위해 useEffect 안에서 TMDB의 /movie/popular 엔드포인트를 호출했다.
useEffect(() => {
  const fetchPopular = async () => {
    setIsLoading(true);

    const params = new URLSearchParams({
      api_key: API_KEY,
      language,
    });

    const res = await fetch(`${POPULAR_URL}?${params}`);
    const data = await res.json();

    setMovies(data.results ?? []);
    setIsLoading(false);
  };

  fetchPopular();
}, [language]);
 
  • 언어를 변경하면 language 값이 바뀌고, 이에 따라 인기 영화 목록도 다시 불러오도록 했다.
  • 검색 기능은 영화 제목 입력, 성인 콘텐츠 포함 여부 체크박스, 언어 선택으로 구성했다.
  • 검색어는 input 상태로 관리했고, 체크박스 값은 TMDB API의 include_adult 파라미터에 반영했다.
  • 언어는 한국어, 영어, 일본어 중 선택할 수 있도록 select 박스를 사용했다.
  • 영화 카드를 클릭하면 상세 모달이 열린다.
  • 모달에는 포스터, 제목, 평점, 개봉일, 인기도, 줄거리, IMDb 검색 버튼을 배치했다.
  • 모달은 닫기 버튼뿐만 아니라 바깥 영역 클릭, ESC 키 입력으로도 닫을 수 있게 만들었다.

컴포넌트 구조

전체 구조는 단순하게 가져갔다.

App
├── SearchForm
├── MovieCard
└── MovieModal
 

App.jsx에서는 영화 목록, 검색어, 언어, 성인 콘텐츠 포함 여부, 선택된 영화 상태를 관리했다.

  • SearchForm은 검색 입력 영역을 담당
  • MovieCard는 영화 카드 하나를 렌더링
  • MovieModal은 선택된 영화의 상세 정보를 보여주는 역할을 한다.

각 컴포넌트는 불필요한 렌더링을 줄이기 위해 React.memo로 감쌌다.

const MovieCard = memo(({ movie, onClick }) => {
  return (
    <article onClick={() => onClick(movie)}>
      {/* 영화 카드 내용 */}
    </article>
  );
});
 

성능 최적화를 적용한 이유

React에서는 부모 컴포넌트의 상태가 바뀌면 부모가 다시 렌더링되고, 기본적으로 자식 컴포넌트도 함께 다시 렌더링된다.

  • 이번 실습에선 검색 input에 글자를 입력할 때마다 App 컴포넌트의 상태가 바뀐다.
  • 그러면 영화 카드 목록이나 모달처럼 실제로 바뀌지 않아도 되는 컴포넌트까지 다시 렌더링될 수 있다.
  • 영화 카드가 몇 개 없을 때는 큰 문제가 되지 않지만, 카드 수가 많아질수록 불필요한 렌더링이 성능에 영향을 줄 수 있다. 그
  • 그래서 React.memo, useCallback, useMemo를 적용해 렌더링 비용을 줄이고자 했다.

React.memo

React.memo는 컴포넌트의 props가 바뀌지 않았다면 이전 렌더링 결과를 재사용한다.

  • 즉, 부모 컴포넌트가 다시 렌더링되더라도 자식 컴포넌트의 props가 그대로라면 자식 컴포넌트는 다시 렌더링되지 않는다. 
  • earchForm, MovieCard, MovieModal에 React.memo를 적용했다.
export default memo(MovieCard);
 

특히 MovieCard는 영화 목록 개수만큼 반복 렌더링되기 때문에, 불필요한 렌더링을 줄이는 효과를 확인하기 좋았다.

useCallback

useCallback은 함수를 메모이제이션할 때 사용한다.

  • React 컴포넌트 내부에서 선언한 함수는 렌더링될 때마다 새로 만들어진다.
  • 문제는 이 함수가 자식 컴포넌트에 props로 전달될 때 발생한다.
  • 자식 컴포넌트를 React.memo로 감싸더라도, props로 전달되는 함수가 매번 새로 생성되면 React는 props가 바뀌었다고 판단한다.
  • 그러면 React.memo를 적용해도 자식 컴포넌트가 다시 렌더링될 수 있다.
  • 그래서 모달 열기, 모달 닫기, 검색 처리 함수 등을 useCallback으로 감쌌다.
const handleOpenModal = useCallback((movie) => {
  setSelectedMovie(movie);
}, []);

const handleCloseModal = useCallback(() => {
  setSelectedMovie(null);
}, []);
 

검색 함수는 검색어, 언어, 성인 콘텐츠 포함 여부에 따라 API 요청 값이 달라지기 때문에 의존성 배열에 해당 값들을 넣어주었다.

 
const handleSearch = useCallback(
  async (e) => {
    e.preventDefault();

    const params = new URLSearchParams({
      api_key: API_KEY,
      query,
      language,
      include_adult: String(includeAdult),
    });

    const res = await fetch(`${SEARCH_URL}?${params}`);
    const data = await res.json();

    setMovies(data.results ?? []);
  },
  [query, language, includeAdult]
);
 

useCallback은 모든 함수에 무조건 사용하는 것이 아니라, 주로 메모이제이션된 자식 컴포넌트에 함수를 props로 넘길 때 효과가 크다.

 

useMemo

useMemo는 계산 결과를 메모이제이션할 때 사용한다. 이번 프로젝트에서는 영화 목록을 평점 내림차순으로 정렬할 때 사용했다.

const sortedMovies = useMemo(() => {
  return [...movies].sort((a, b) => b.vote_average - a.vote_average);
}, [movies]);
 

sort()는 배열을 직접 변경하기 때문에 원본 movies를 그대로 정렬하지 않고, 스프레드 문법으로 복사한 뒤 정렬했다.

  • 이렇게 하면 movies 배열이 바뀔 때만 정렬이 다시 실행된다.
  • 검색어 입력처럼 영화 목록 자체가 바뀌지 않는 렌더링에서는 이전에 계산된 정렬 결과를 그대로 사용할 수 있다.

세 가지의 차이

구분 역할 사용하기 좋은 상황
React.memo props가 바뀌지 않으면 컴포넌트 리렌더링을 건너뜀 자식 컴포넌트가 자주 불필요하게 렌더링될 때
useCallback 함수 참조를 유지함 memo 처리된 자식에게 함수를 props로 넘길 때
useMemo 계산 결과를 재사용함 정렬, 필터링, 복잡한 계산을 매번 반복하고 싶지 않을 때

느낀 점은 최적화 훅을 무조건 많이 쓰는 것이 좋은 것은 아니라는 점이다.

  • useCallback과 useMemo도 내부적으로 값을 기억하는 비용이 있다
  • 단순한 코드에 무작정 적용하면 오히려 코드만 복잡해질 수 있다.
  • 하지만 영화 카드처럼 반복 렌더링되는 컴포넌트가 있고, 정렬처럼 반복 계산되는 작업이 있다면 적절히 사용하는 것이 도움이 된다.