[트러블슈팅] Next.js 스크립트 reload 오류

liinyeye·2024년 11월 11일

인턴 생활

목록 보기
1/2

🔎 문제 상황

한 번 SDK 스크립트가 로드 된 상황에서 뒤로가기 버튼을 눌러 다시 카메라 페이지로 페이지 이동 시, SDK 카메라가 켜지지 않는다.

상황 분석

  • 뒤로 가기 버튼 눌렀을 때, 로딩 페이지로 간다.
  • 여기서 SDKInitializer가 동작하면서 스크립트가 로드된다.

✅ 문제는 이미 한번 스크립트가 로드되었기에, 페이지 이동 시 컴포넌트가 새롭게 마운트되지 않으면 스크립트가 다시 로드되지 않아서 SDK 카메라가 제대로 동작되지 않게 된다.

❗️ Next.js에서는 Script 태그는 Next.js의 최적화된 스크립트 관리 방식에 따라 한 번 로드된 스크립트를 캐싱 및 재사용하여 페이지 이동 시 리로드되지 않는다.

❗️ 페이지 이동 후 스크립트 초기화 동작이 필요한 경우, 기존 스크립트 제거 및 재로드가 필요함

https://stackoverflow.com/questions/73221131/next-js-reload-script-tag

💡 해결 방법

스크립트 로드 시 Next.jsScript 태그 대신에, 순수 자바스크립트로document.createElement를 사용해 스크립트를 동적으로 추가하는 방식으로 변경한다.

  • 이렇게 하면 컴포넌트가 다시 마운트될 때마다 스크립트가 새로 로드된다.
  • SDK 카메라가 잘 켜지게 된다.

코드 참고

⚠️ Next.js의 Script 태그를 사용한 기존 코드

스크립트의 key를 다르게 하여 페이지 전환이 있을 때마다 고유한 스크립트를 로드하도록 시도

const SDKInitializer = () => {
  const [sdkInitStatus, setSdkInitStatus] = useRecoilState(sdkInitState);
  const [scriptVersion, setScriptVersion] = useState(Date.now());

    // 스크립트 재로드가 필요할 때 호출
    const reloadScripts = () => {
      setScriptVersion(Date.now());
    };

    const handleSDKLoad = () => {
      setSdkInitStatus((prev) => ({
        ...prev,
        isScriptLoaded: true,
        isInitialized: false,
        progress: 0,
      }));
    };

    return (
      <>
        <Script
          key={`A-${scriptVersion}`}
          src="..."
          strategy="afterInteractive"
          onLoad={() => console.log('A script loaded')}
        />
        <Script
          key={`B-${scriptVersion}`}
          src="..."
          strategy="afterInteractive"
          onLoad={() => ( 
            console.log('A script loaded')
            () => handleSDKLoad())}
        />
      </>
    );

export default SDKInitializer;

⭕️ 동적 스크립트 로드 방식 코드

const SDKProvider = ({ children }: SDKProviderProps) => {
  const [sdkInitStatus, setSdkInitStatus] = useRecoilState(sdkInitState);
  const pathname = usePathname();

  // try-on 관련 페이지에서만 초기화되도록
  const shouldInitSDK = useCallback(() => {
    return pathname === '/try-on' || pathname === '/try-on-loading';
  }, [pathname]);

  // 동적으로 스크립트 추가
  const loadScript = useCallback((src: string) => {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = src;
      script.async = true;
      script.onload = resolve;
      script.onerror = reject;
      script.id = `sdk-script-${src.split('/').pop()}`;
      document.body.appendChild(script);
    });
  }, []);

  // 스크립트 초기화 로직
  const initializeSDK = useCallback(async () => {
    try {
      setSdkInitStatus((prev) => ({
        ...prev,
        isScriptLoaded: false,
        isInitialized: false,
        progress: 0,
      }));

      await Promise.all([
        loadScript(SDK_SCRIPTS.A),
        loadScript(SDK_SCRIPTS.B),
      ]);

      setSdkInitStatus((prev) => ({
        ...prev,
        isScriptLoaded: true,
      }));
    } catch (error) {
      console.error('Failed to load SDK scripts:', error);
      setSdkInitStatus((prev) => ({
        ...prev,
        error: error instanceof Error ? error.message : 'An unknown error occurred',
      }));
    }
  }, [loadScript, setSdkInitStatus]);

  useEffect(() => {
    if (shouldInitSDK()) {
      initializeSDK();
    }

    // 언마운트 시 스크립트 제거로 메노리 누수 방지
    return () => {
      if (!shouldInitSDK()) {
        const scripts = document.querySelectorAll('script[id^="sdk-script-"]');
        scripts.forEach((script) => script.remove());

        setSdkInitStatus((prev) => ({
          ...prev,
          isScriptLoaded: false,
          isInitialized: false,
          progress: 0,
          error: null,
        }));
      }
    };
  }, [pathname, shouldInitSDK, initializeSDK]);

  return <>{children}</>;
};

export default SDKProvider;

스크립트 전역 설정

'use client';

import { CustomToastContainer, SDKProvider } from '@/components';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { PropsWithChildren, useState } from 'react';
import { RecoilRoot } from 'recoil';
import 'react-toastify/dist/ReactToastify.css';

const Providers = ({ children }: PropsWithChildren) => {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: Infinity,
          },
        },
      }),
  );
  return (
    <RecoilRoot>
      <SDKProvider>
        <QueryClientProvider client={queryClient}>
          <ReactQueryDevtools initialIsOpen={false} />
          <CustomToastContainer />
          {children}
        </QueryClientProvider>
      </SDKProvider>
    </RecoilRoot>
  );
};

export default Providers;

🚀 결과

1. SDK 스크립트 로드 로직이 중앙집중화됨

2. try-on 관련 페이지에서만 SDK가 로드됨

3. 다른 페이지로 이동 시 자동으로 SDK가 정리됨 -> 스크립트 리소스 정리

4. 페이지 새로고침이나 직접 URL 접근 시에도 SDK가 적절히 초기화됨


참고 자료

profile
웹 프론트엔드 UXUI

0개의 댓글