2024. 2. 22. 18:21ㆍReact JS
리액트는 client side rendering, SPA 라이브러리이다.
그렇기 때문에 처음에 화면을 그릴 때에 public/index.html에 있는 자원들을 모두 읽어온다. javascript 파일들 역시 읽어오고 실행한다. 특히 애니메이션 효과를 담당하는 javascript 파일 내부에는 이미 렌더링 된 DOM 중 클래스명 등으로 지정된 특정한 DOM에 애니메이션 이벤트를 추가하거나(addEventListener(...)) 하는 코드가 포함되어 있다.
첫 화면 진입 이후 화면전환 시, 위에 적힌 종류의 자바스크립트 파일을 불러오는 방식에서 일반 html 파일과 리액트 app 차이가 발생한다. (mpa, spa, client side rendering, server side rendering 같이 웹페이지를 보여주는 방식에 따라 다양하게 나뉘기 때문에 꼭 일반 html, 리액트 App 이라고 싸잡아 말하기에는 그렇지만 말이다. 크게 퍼블로 주는 html 파일과 데이터가 interactive하게 작동함에 따라 전체 ui 상태가 아닌 일부 ui만 다시 렌더링하는 App과의 차이라고 보면 될 것 같다.)
일반 html 혹은 데이터가 변경될 때 서버에서 전체 페이지를 새롭게 렌더링해서는 주는 경우, 애니메이션 효과를 주는 javascript 파일을 실행하기 전에 이미 애니메이션을 주어야할 컴포넌트 혹은 데이터가 존재한다. 그렇기 때문에 애니메이션 효과가 정상적으로 작동한다.
하지만 보편적인 리액트 app 과 같이 웹사이트에 처음 진입했을 때 index.html에 적힌 public 내의 css와 javascript 파일 등 모든 자원을 읽어오는 웹사이트의 경우, 이미 애니메이션을 주는 javascript 파일을 읽어왔기 때문에 다른 화면으로 진입했거나 데이터 상태 값에 변경에 따라 ui가 바뀌어도 컴포넌트의 애니메이션 동작이 예상대로 되지 않는다.
그렇기 때문에 데이터의 변경이 발생하고 이에 따라 애니메이션 효과를 주려면, 애니메이션을 실행하는 javascript 파일을 불러와야할 때가 있다.
그래서 아래와 같이 react hook을 만들어 두면 편하다. 코드에 주석들도 적어놓았으니 한번 읽어보면 좋겠다.
import { useCallback, useEffect, useState } from 'react';
type ScriptStatus = 'idle' | 'loading' | 'ready' | 'error';
export const useScript = (basePath = 'assets/') => {
// 스크립트 파일을 불러오는 상태
const [status, setStatus] = useState<ScriptStatus>('idle');
// public에 있는 순수 자바 스크립트 파일을 불러오는 함수
const loadScript = useCallback(
(src: string) => {
const existingScript = document.querySelector(`script[src="${src}"]`);
// 불러올 자바스크립트 파일을 불러오는 script 태그가 이미 존대한다면 삭제한다.
// 중복된 요소를 여러개 불러오면 가독성 해치고 프로그램도 무거워질 것이므로.
if (existingScript) {
const existingStatus = existingScript.getAttribute('data-status');
document.body.removeChild(existingScript);
if (existingStatus) {
// setStatus(existingStatus as ScriptStatus);
// return existingScript as HTMLScriptElement;
}
}
// 스크립트 태그 생성
const script = document.createElement('script');
script.src = src;
script.async = true;
// data-status 를 loading으로
script.setAttribute('data-status', 'loading');
// 스크립트 태그에 event listener 추가
// javascript 파일을 모두 읽어들였을 시 data-status 를 ready로
script.addEventListener('load', () => {
script.setAttribute('data-status', 'ready');
setStatus('ready');
});
// // javascript 파일을 읽어들이는 중 오류 발생 시 data-status 를 error로
script.addEventListener('error', () => {
script.setAttribute('data-status', 'error');
setStatus('error');
});
// html.body에 script 태그를 추가 및 해당 스크립트 파일 읽어들이기 시작
document.body.appendChild(script);
},
[setStatus],
);
// 리액트 컴포넌트에서 호출하여 인자로 들어온 javascript 파일을 불러오는 함수
const importScript = useCallback(
(jsSrc: string | string[]) => {
const scripts = Array.isArray(jsSrc) ? jsSrc.map((js) => `${basePath}${js}.js`) : [`${basePath}${jsSrc}.js`];
scripts.forEach(loadScript);
},
[loadScript, basePath],
);
// 이 훅이 unmount 될 때에, 주로 이 훅을 사용하고 있는 컴포넌트가 unmount 될 때에, 그러니까 화면이 전환될 때에
// 'script[data-status="loading"], script[data-status="ready"], script[data-status="error"]'로 되어있는 태그는
// 여기서 만들어진 것이기 때문에 지워준다.
// *** 다만 프로젝트마다 불러들인 script 태그를 지워야할 시점과 이 hook이 unmount 될 때에 실행할 액션이 다를 수도 있므로 참고만 하시기 바랍니다.
useEffect(() => {
return () => {
const scriptElements = document.querySelectorAll('script[data-status="loading"], script[data-status="ready"], script[data-status="error"]');
scriptElements.forEach((script) => {
document.body.removeChild(script);
});
};
}, []);
// 종종 리액트 컴포넌트 내에서 여기에 src로 들어온 javascript 파일을 다 불러온 다음에
// 무언가 이벤트를 실행하도록 해야하는 경우도 있으므로 status도 함께 return 시켜준다.
return { status, importScript };
};
예제로 count가 바뀌면 theme.js를 불러오는 컴포넌트를 만들어보았다.
import React, { useLayoutEffect, useState } from "react";
import { useScript } from "../hooks/common/ScriptHook";
const SandBoxComponent = () => {
const { importScript } = useScript();
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prev) => prev + 1);
};
useLayoutEffect(() => {
importScript(["js/theme"]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count]);
return <button onClick={handleClick}>count : {count}</button>;
};
export default SandBoxComponent;
아래는 index.html 의 body 부분이다. 처음에 theme.js, main.js를 순서대로 읽어오고 있다.
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="assets/js/theme.js"></script>
<script src="assets/js/main.js"></script>
</body>
그리고 각 js 파일은 이런 콘솔을 찍는다.
// main.js
console.log("main.js 입니다.");
// theme.js
console.log("theme.js 입니다.");
자, 이제 화면을 켜보자.
theme.js가 리액트 애플리케이션이 켜질 때 한번, SandBoxComponent 가 mount 됨에 따라 한번 이렇게 import 되었다.
이제 SandBoxComponent에 설계된 대로 count 버튼을 눌렀을 때 theme.js가 새롭게 불러와져야 한다.
count 버튼을 누를때마다 "theme.js 입니다" 콘솔이 찍히고 <script src="assets/js/theme.js" async="" data-status="ready"></script> 태그가 만들어지는 것을 볼 수있다.
깃허브: https://github.com/kingkiboots/react-import-public-js
수정사항:
- FIX) 240426 : useScript에서 useEffect를 useLayoutEffect로 변경
useEffect => UI 렌더링 상관없이 비동기로 기존의 스크립트를 지움
그래서 화면전환 시 화면이 unmount 될 때에 비동기로 시작하므로 새로운 화면이 mount 된 이후에 생긴 새 script도 remove 하는 현상 발생
useLayoutEffect => 동기로 기존의 스크립트를 지움. 다 지운 다음에 렌더링 하므로 위의 현상 수정
'React JS' 카테고리의 다른 글
[React JS] React 로 MPA(Multi Page Application) 구현하기 (1) | 2024.04.22 |
---|---|
[React JS] Yarn 명령어 실행하는데 self signed certificate 에러가 날 경우 (0) | 2024.04.12 |
[React JS] webpack으로 build 시 html에 환경변수 넣는 법 (1) | 2024.01.26 |
[React JS] Spring Boot 내에서 React 어플리케이션 구동시키기 (2) | 2024.01.25 |
[React JS]Jquery fadeIn/Out javascript로 변경하기 (0) | 2023.12.18 |