Skip to content
Go back

리액트에서 외부 스크립트 로드하기

Edit page

들어가기

React 프로젝트에서 외부 스크립트를 사용할 때 보통 npm에서 패키지를 설치해 사용합니다.

하지만 모든 서비스가 npm 패키지를 제공해 주지는 않습니다. 이번 프로젝트에서 사용한 Daum 우편번호 서비스와 Kakao Maps API도 마찬가지입니다.

Kakao Maps API 공식 가이드에서 가져온 지도를 띄우는 코드 예시입니다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Kakao 지도 시작하기</title>
  </head>
  <body>
    <div id="map" style="width:500px;height:400px;"></div>
    <script
      type="text/javascript"
      src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY를 넣으시면 됩니다."
    ></script>
    <script>
      var container = document.getElementById("map");
      var options = {
        center: new kakao.maps.LatLng(33.450701, 126.570667),
        level: 3,
      };

      var map = new kakao.maps.Map(container, options);
    </script>
  </body>
</html>

이걸 어떻게 Next.js 프로젝트에 적용할지 고민했습니다. react-kakao-maps-sdk 같은 라이브러리도 존재했지만, 지도를 간단히만 사용할 예정이라, 프로젝트의 기능에 맞게 사용 흐름을 직접 설계해보기로 했습니다.

그 과정에서 알게된 script 태그를 React 프로젝트에서 사용하는 방법을 이 글에서 소개하겠습니다.

HTML 파일에 추가하기

index.html 파일에 script 태그를 넣어 줄 수 있습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
  </head>
  <body>
   <script src="//dapi.kakao.com/v2/maps/sdk.js?appkey="></script>
    <div id="root"></div>
</html>

페이지가 로드될 때 스크립트를 내려받아 준비하고, 그 뒤에 React 앱이 생성됩니다.

이 방법은 페이지가 열릴 때마다 반드시 실행돼야 하는 스크립트(e.g. 통계나 모니터링용 스크립트처럼 항상 로드되어야 하는 코드)에 적용하기 좋습니다.

하지만 이 방식은 다음과 같은 경우 세밀한 제어를 하기 어렵습니다.

특히 우리 서비스의 경우에는 세션 상세 페이지에서만 지도가 필요합니다. 이 방식을 사용하면 지도와는 상관없는 초기 페이지에서 사용자에게 먼저 보여줘야 할 더 중요한 정보들이 늦게 렌더링될 수 있습니다.

자바스크립트로 script 태그를 동적으로 삽입하기

HTML을 직접 수정하지 않고, 자바스크립트에서 <script> 태그를 동적으로 만들어서 삽입하는 방법입니다.

const script = document.createElement("script");
script.src = "//dapi.kakao.com/v2/maps/sdk.js?appkey=";
script.async = true;
document.body.appendChild(script);

React 컴포넌트에서 script 태그를 동적으로 삽입하기

앞에서 본 방법을 React 프로젝트에 적용해보겠습니다.

import { useEffect } from "react";

export default function Page() {
  useEffect(() => {
    const script = document.createElement("script");
    script.src = "//dapi.kakao.com/v2/maps/sdk.js?appkey=";
    script.async = true;
    document.body.appendChild(script);
  }, []);

  return <>page</>;
}

useEffect를 사용해 컴포넌트가 마운트될 때 스크립트를 추가하고, 빈 의존성 배열을 넣어서 한 번만 실행되도록 했습니다.

이 방법에도 한 가지 문제가 있습니다. 바로 중복 생성 가능성입니다.

이 코드에서는 그런 상황에서도 매번 document.createElement(“script”)를 호출하게 되고, 동일한 스크립트 태그가 여러 번 추가될 수 있습니다.

script 태그 중복 생성 방지하기

  1. 이미 같은 스크립트가 있는지 먼저 확인하고
  2. 없을 때만 새로 생성하는 방식으로

조금 더 방어적으로 코드를 작성해 줄 필요가 있습니다.

import { useEffect } from "react";

const KAKAO_MAP_SCRIPT_ID = "kakao-map-sdk";

export default function Page() {
  useEffect(() => {
    const existingScript = document.getElementById(KAKAO_MAP_SCRIPT_ID);
    if (existingScript) return;

    const script = document.createElement("script");
    script.id = KAKAO_MAP_SCRIPT_ID;
    script.src = "//dapi.kakao.com/v2/maps/sdk.js?appkey=";
    script.async = true;
    document.body.appendChild(script);
  }, []);

  return <>page</>;
}

DOM에 스크립트 요소가 추가되어 있으면 로드가 완료되었다고 보고, 스크립트를 이미 추가한 적이 있는지를 먼저 체크하는 코드입니다.

이렇게 하면

스크립트를 불러온 다음엔?

외부 스크립트를 불러오기만 해서는 충분하지 않습니다. 우리는 스크립트가 노출해 주는 전역 객체나 함수를 사용하려고 로드했으니까요.

하지만 <script> 로드가 언제 완료될지 알 수 없기 때문에, 스크립트가 아직 로드되기 전에 window.kakao에 접근하면 에러가 발생할 수 있습니다. 즉, 스크립트가 완전히 준비된 뒤에만 코드를 실행한다는 걸 보장해 줘야 합니다.

load 이벤트는 스크립트가 성공적으로 로드되었을 때 발생하는 이벤트입니다. 이 이벤트가 발생한 이후에 kakao를 사용하게끔 만들면 되겠네요!

import { useEffect, useState } from "react";

const KAKAO_MAP_SCRIPT_ID = "kakao-map-sdk";

export default function Page() {
  const [loaded, setLoaded] = useState(false);
  const mapRef = useRef<kakao.maps.Map | null>(null);
  // 카카오맵 스크립트를 로드함
  useEffect(() => {
    const existingScript = document.getElementById(KAKAO_MAP_SCRIPT_ID);

    if (existingScript) {
      setLoaded(true);
      return;
    }

    const script = document.createElement("script");
    script.id = KAKAO_MAP_SCRIPT_ID;
    script.src = "//dapi.kakao.com/v2/maps/sdk.js?appkey=";
    script.async = true;
    const handleLoad = () => setLoaded(true);
    script.addEventListener("load", handleLoad);
    document.body.appendChild(script);

    return () => {
      script.removeEventListener("load", handleLoad);
    };
  }, []);

  // 지도를 그림
  useEffect(() => {
    // 스크립트가 준비되지 않으면 실행하지 않음
    if (!loaded || !window.kakao?.maps) {
      console.warn("Kakao Map is not loaded yet.");
      return;
    }

    new kakao.maps.Map(mapRef.current, {
      center: new kakao.maps.LatLng(33.450701, 126.570667),
    });
  }, []);

  return <div ref={mapRef}></div>;
}

나중엔 error 이벤트를 활용해서 스크립트 로드가 실패했을 때 에러 처리를 추가하고 스크립트를 다루는 로직을 커스텀 훅으로 분리하면 좋겠네요.

Next.js에서 사용하기

Next.js에서는 script 태그를 직접 다루지 않고도 쉽게 관리할 수 있도록 Script 컴포넌트를 제공합니다. 기본 <a> 대신 Link 컴포넌트를 쓰는 것처럼, <script> 대신 Script 컴포넌트를 쓰는 느낌이라고 보면 됩니다.

앞에서 고민했던 스크립트를 중복으로 추가하지 않고, 한 번만 로드하고 싶다는 문제를 Next.js가 어느 정도 대신 처리해 줍니다.

공식 문서에서도 이렇게 설명하고 있습니다:

Next.js will ensure the script will only load once, even if a user navigates between multiple routes in the same layout.

또한 스크립트 로드와 관련된 주요 이벤트들도 아래와 같은 props로 다룰 수 있습니다.

공식 문서 예시는 다음과 같습니다.

import Script from "next/script";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <section>{children}</section>
      <Script src="https://example.com/script.js" />
    </>
  );
}

참고 및 관련 자료


Edit page
Previous Post
useMediaQuery 훅 구현하기: useEffect에서 useSyncExternalStore로
Next Post
TailwindCSS를 직접 만들어보고 동작 원리 이해하기