Tech

Javascript를 이용하여 JS 파일 동적 로딩

Jamsun2 2022. 7. 29. 12:10

개요

고객사의 홈페이지에 javascript 파일을 로딩할 일이 생겼습니다. 
단순하게 script 태그를 이용하여, 해당 페이지에 삽입하였고, 스크립트 내부 함수를 호출하는 간단한 구조로 작성되었습니다.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
        <script type="text/javascript" src="https://example.com/example.js" charset="utf-8"></script>
    </head>
    <body>
        <script type="text/javascript">
            init();
        </script>
    </body>
</html>

Head에 스크립트를 삽입하고, body에서 내부 함수를 호출하는 간단한 구조였습니다.

 

 

문제점 발생

고객사에서 스크립트에 대한 안정성에 문제가 있다는 통보를 받았습니다.

홈페이지 특성상, 외부 접속이 자유롭지 않은 상황에서 스크립트 로딩 지연이 생길 수 있고, 해당 증상으로 홈페이지가 정상적으로 제공되지 않을 수 있으니, 대책을 마련해달라고 했습니다.

주요 내용은, 스크립트 로딩 대기 시간이 15초가 발생하면, 홈페이지가 15초 동안 아무 동작도 하지 않는 문제가 있다는 것이었습니다.

 

 

해결방안 모색

처음에는 단순하게, 그럼 해당 홈페이지의 제일 하단에서 호출하면 문제가 없을 것이라고 가이드를 제공했습니다.

하지만, 고객사 웹페이지에서 항상 최하단에 스크립트를 위치시키는 데에는 무리가 있었습니다. 

다음 방법으로, 비동기 방식으로 스크립트를 호출할 수 있도록 변경했습니다.

    <head>
        <script async type="text/javascript" src="example.js" charset="utf-8"></script>
    </head>
    <body>
        <script type="text/javascript">
            init();
        </script>
    </body>

간단하게, async 정보를 스크립트에 추가해주는 방법입니다.

 

 

여기서, 스크립트가 비동기 방식으로 호출되면서, 호출 함수와 싱크가 맞지 않는 문제가 발생했습니다.

(index):11 Uncaught ReferenceError: init is not defined
    at (index):11:13

이런 메시지를 만나게 됩니다.

스크립트가 로딩되기 전에 함수가 호출된 것입니다.

해당 이슈를 해결하기 위한 제일 간단한 방법은 스크립트가 호출될 때까지 잠시 기다려주는 방법입니다. 

스크립트가 로딩되었을 것이라고 생각될 만큼의 timeout을 주고 함수를 호출하는 로직을 추가합니다.

        <script type="text/javascript">
            function init_call(){init();}
            setTimeout(init_call,100);
        </script>

 

 

정상 동작하는 것처럼 보였는데, 가끔, 운이 나쁘게 100ms 이상의 로딩 시간이 걸리면, 함수 호출에 실패합니다.

그래서, 함수가 로딩되었는지 검사 로직을 추가했습니다.

        <script type="text/javascript">
            function init_call(){
                if(typeof init == "function"){ init(); }
            }
            setTimeout(init_call,100);
        </script>

자 이제, 고객사의 홈페이지에 영향을 주지 않고, 스크립트를 로딩한 후에, init 함수를 호출할 수 있게 되었습니다. 그런데, 여기에는 두 가지 문제가 있었습니다. 하나는, init 함수는 항상 호출되어야 하는데, 해당 코드는 가끔 호출되지 않을 때가 있는 문제가 있습니다. 또한, 항상 100ms의 호출 delay가 발생합니다.

 

 

이러한 문제를 해결하기 위해, 스크립트 태그 자체의 로딩 시점에 호출되는 내부 함수를 사용하기로 했습니다.

<head>
	<script async type="text/javascript" src="example.js" charset="utf-8" onload='if(typeof init == "function"){ init(); }'></script>
</head>

최종적으로, 모든 역경을 이겨내고, 이런 코드가 만들어집니다. 이제, 스크립트가 비동기로 동작하면서, 로딩 시점에 init 함수를 호출할 수 있는 모양이 완성되었습니다.

 

 

JS 스크립트 동적 로딩

잘 넘어가나 싶었는데, 고객사에서 또 다른 요청이 들어옵니다.

고객사의 페이지가 1만 개가 넘는데, 그 스크립트를 다 넣는 것은 무리가 있으니, 마침 고객사 스크립트가 들어가 있는 것도 있고 하니, 해당 스크립트에서 동적으로 호출하게 해 달라는 요청이었습니다.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
        <!-- 고객사에서 제공한 모든 페이지에 들어가 있는 all_page.js-->
        <script type="text/javascript" src="all_page.js" charset="utf-8" ></script>
    </head>
    <body>
    </body>
</html>
<!-- all_page.js 파일 내부 -->
function init(){
    console.log("load all_page.js, success!");
}
init();

 

고객사의 모든 페이지에 들어가 있는 all_page.js 파일입니다. 우리는 해당 js파일에서 우리 스크립트를 동적으로 호출하는 방법을 찾아야 했습니다. 일단, 우리 스크립트 파일의 호출 함수 이름을 겹치지 않도록 변경합니다.

<!-- example.js 파일 내부 -->
function my_example_init(){
    console.log("load init() function, success!");
}

 

그다음, head 태그 아래에 스크립트를 삽입하고, 스크립트가 로딩이 끝나면, 함수를 호출하도록 작업합니다.

<!-- all_page.js 내부 -->
function init(){
    console.log("load all_page.js, success!");
}
init();

var my_head = document.getElementsByTagName('head')[0];
var my_js = document.createElement('script');
my_js.type= 'text/javascript';
my_js.async = true;
my_js.src = 'example.js';
my_js.onload = function (){if(typeof my_example_init == "function"){my_example_init();} };
my_head.appendChild(my_js);

 

Head 태그를 찾아서, 우리 스크립트를 마지막 항목에 추가해줍니다.

비동기 방식으로 로딩되도록 하고, 로딩이 끝나면, 내부 함수를 호출해주도록 추가했습니다.

 

이제, 작업이 끝나가네요.

기존에 있던 코드와 분리해서, 나중에 유지보수가 편하도록, 코드 분리만 해주면 될 것 같습니다.

(function() {
    var my_head = document.getElementsByTagName('head')[0];
    var my_js = document.createElement('script');
    my_js.type= 'text/javascript';
    my_js.async = true;
    my_js.src = 'example.js';
    my_js.onload = function (){if(typeof my_example_init == "function"){my_example_init();} };
    my_head.appendChild(my_js);
})();

 

자, 이제 깔끔하게 고객사 코드와 우리 코드가 분리되었습니다.

추가로, 내부의 스크립트 정보와 호출 함수를 입력값으로 받을 수 있으며, onload 대신 eventListener를 사용할 수 있습니다.

스크립트의 중복 실행 방지도 추가해줘야겠죠.

<!-- all_page.js 내부 -->
function init(){
    console.log("load all_page.js, success!");
}
init();

(function(id, js_src, callback) {
    if (document.getElementById(id)) { return; }
    var my_head = document.getElementsByTagName('head')[0];
    var my_js = document.createElement('script');
    my_js.id = id;
    my_js.type= 'text/javascript';
    my_js.async = true;
    my_js.src = js_src;
    my_js.addEventListener("load", function(event) { if(callback && typeof callback == "function"){ callback(); }});
    my_head.appendChild(my_js);
})('MY_EXAMPLE_SCRIPT','example.js', function (){if(typeof my_example_init == "function"){my_example_init();} });

실제 개발자 모드에서 실행해보면, 스크립트가 지면에 추가된 것을 확인할 수 있습니다.

<!DOCTYPE html>
<html lang="en"><head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
        <!-- 고객사에서 제공한 모든 페이지에 들어가 있는 all_page.js-->
        <script type="text/javascript" src="all_page.js" charset="utf-8"></script>
        <script id="MY_EXAMPLE_SCRIPT" type="text/javascript" async="" src="example.js"></script>
    </head>
    <body>
    
</body></html>

이상, Javascript 내에서 js 파일을 동적 로딩하는 방법에 대한 설명이었습니다.

 

참고

본 문서에서는 다루지 않았지만,

- async 옵션 대신, defer 옵션을 사용할 수 있습니다. defer는 모든 페이지 항목의 로딩이 끝나면, 해당 스크립트를 로딩하게 됩니다.

- 하위 버전의 브라우저는 addEventListener 대신, attachEvent를 사용합니다.

- 항목 추가 시, 제일 앞에 삽입하는 경우, insertBefore를 사용하면 됩니다.

 

-- written by tech.po@tg360tech.com --