Web UI

raccoonback·2020년 6월 16일
1

boost course

목록 보기
4/10
post-thumbnail

디렉토리 구성

HTML에서 Javascript, CSS 같은 코드는 어떻게 관리하면 좋을까?

우선 Javascript 코드가 많지 않다면, HTML에 포함시키는 것도 고려해 볼 수 있다.
하지만, 코드가 방대해진다면 관리하기 쉽지 않기 때문에 별도의 파일로 분리하는 것을 권장한다.

브라우저는 파일의 상단부터 한 라인씩 코드를 파싱하는데, Javascript 코드(<script>)가 있는 경우에는 DOM Tree 구성 작업을 중단하고 <script> 코드를 먼저 실행한다. 이 과정에서 몇 가지 문제가 발생할 수 있다. 예를 들어, Javascript 코드가 상단에 위치에서 특정 element를 찾고자 하는 경우, DOM이 모두 구성되지 않았기 때문에 API를 통해 element 발견하지 못하는 이슈가 발생할 수 있다.(이러한 경우, DOMContentLoaded 이벤트에 대한 핸들러로 등록해 사용)

아래 예시를 보면, 첫 번째 javascript 코드는 h1 요소가 렌더링되기 이전에 탐색했기 때문에 null을 반환한다.
하지만, 두 번째에서는 h1 요소가 렌더링된 상태이기 때문에 정상적으로 탐색을 마칠 수 있다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
    <style>
        h1 {
            background-color: brown;
        }
    </style>
</head>
<body>
    <script>
        const front = document.querySelector('h1'); // h1 태그를 찾지 못한다.
        console.log(front);
    </script>
    <h1> hello world </h1>
    <script>
        const end = document.querySelector('h1');
        console.log(end);
    </script>
</body>
</html>

따라서 분리한 CSS 파일은 <link rel="stylesheet" href="경로"> 이용해 <head> 태그안에 포함시키는 것을 권장하고,

Javascript 파일은 <script scr="경로"> 이용해 <body> 태그가 닫히기 전에 배치하는 것을 권장한다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
    <link rel="stylesheet" href="./css/test.css" >
</head>
<body>
    <h1> hello world </h1>
    <script src ="js/test.js"></script>
</body>
</html>

defer, async 옵션

추가적으로, <script scr="경로">defer, async 옵션을 추가할 수 있다.

  • defer 옵션은 스크립트를 백그라운드에서 로드하기 때문에 스크립트가 무거운 경우 HTML 파싱한다. 로드된 스크립트는 DOM Tree가 완성되고 DOMContentLoaded 이벤트가 발생하기 이전 시점에 실행이 된다.
  • async 옵션은 defer 옵션과 동일하게 스크립트를 백그라운드에서 로드하지만, 페이지와 완전히 독립적으로 동작한다. 즉, 스크립트를 빠르게 로드한 경우에는 HTML 파싱을 멈추게 되고, DOM Tree 완성후 로드가 완료된 경우에는 DOMContentLoaded 이벤트와 상관없이 아무 시점 관계없이 실행된다.

참고자료

DOMContentLoaded 이벤트

브라우저에서의 HTML 렌더링은 다음과 같다.

DOM Tree 구축을 위한 HTML 파싱 => Render Tree 구축 => Layout(Render Tree 배치) => Paint(Render Tree 그리기)

각 단계에서 자세한 설명은 다음에 하기로 한다.
여기서 DOM Tree 구축을 위한 HTML 파싱 작업이 완료됐다는 것을 알려주는 이벤트가 바로 DOMContentLoaded 이다.

DOMContentLoaded 이벤트

구체적으로 보면,DOMContentLoaded 는 브라우저가 HTML을 모두 파싱하고 DOM Tree가 완전하게 구성되어 API를 통해 DOM 조작할 수 있는 시점을 알려주는 이벤트이다.(DOMContentLoaded 이벤트는 document 객체에서 발생한다.)
즉, HTML 파싱 과정에서 Javascript 코드인 <script> 만나면 파싱을 멈춘다고 했다. 하지만, DOMContentLoaded 이벤트가 등록되어 있다면, 해당 Javascript 코드를 실행을 DOM Tree 완료된 이후로 미루고 HTML 파싱을 다시 시작한다.

load 이벤트

load 는 DOM Tree가 모두 구성될 뿐만 아니라 CSS, Image 과 같은 외부 자원도 모두 로드한 시점을 알려주는 이벤트이다.

beforeunload/unload 이벤트

beforeunload/unload 는 사용자가 페이지를 떠나는 시점을 알려주는 이벤트이다.

왜 필요한가

DOMContentLoadedload 이벤트에 대한 Handler를 등록하여 에러없이 원하는 시점 필요한 작업을 수행할 수 있다. 예를 들어, DOMContentLoaded 이벤트 이용해서 HTML Parsing이 끝난 시점에서 서버에 AJAX 통신을 요청할 수 있기 때문에 성능에 유리하다.

이제 코드로 비교해보자.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <script>
        document.addEventListener('DOMContentLoaded', ()  => {
           console.log('dom load')
        });

        window.addEventListener('load', ()  => {
            console.log('page load')
        });
    </script>
    <h1> hello world </h1>
    <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg">
    <script>
        console.log('js code');
    </script>
</body>
</html>
// js code
// dom load
// page load

위와 같이, DOMContentLoaded 이벤트는 항상 load 이벤트보다 먼저 발생하고, console.log 함수는 HTML 파싱을 멈추고 실행되므로 가장 먼저 출력된다.

참고자료

Event delegation

만약 이미지 리스트에서 각 이미지 클릭시, 이미지 URL을 보여주는 기능이 필요하다고 가정해보자.

그럼, 우선 직관적으로 모든 이미지 Element에 대한 addEventListener() 함수에 click 이벤트 핸들러를 등록하는 것을 생각할 수 있다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
    <style>
        li {list-style:none;}

        ul > li {
            display:inline-block;
            padding:10px;
            border:1px solid gray;
        }

        .image {
            width:200px;
            height:auto;
        }
    </style>
</head>
<body>
<ul>
    <li>
        <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
    </li>
    <li>
        <img src="https://image.dongascience.com/Photo/2019/05/3e95c45fbe6710365e999ebbd32ed37e.jpg" class="image">
    </li>
    <li>
        <img src="https://pds.joins.com/news/component/htmlphoto_mmdata/201906/18/f3eb4268-ecf0-40c6-a23a-62dd99d0d5fd.jpg" class="image">
    </li>
    <li>
        <img src="https://img.hani.co.kr/imgdb/resize/2017/1121/00504228_20171121.JPG" class="image">
    </li>
    <li>
        <img src="https://blog.hmgjournal.com/images/contents/article/201603211108-Reissue-pet-family-01.jpg" class="image">
    </li>
</ul>

<div></div>

<script>
    const target = document.querySelector('div');
    const images = document.querySelectorAll('img');
    for(let image of images) {
        image.addEventListener('click', function(event) {
            target.innerHTML = "Image url : " + event.currentTarget.src ;
        });
    }
</script>
</body>
</html>

위 예제와 같이, 모든 Image에 대한 <li> Element 리스트 찾아서 이벤트를 등록하여 처리하였다. 하지만, 이러한 방법은 리스트의 크기가 길어질 경우 메모리를 많이 차지하고 브라우저에서 관리해야 하는 핸들러가 증가하게 된다.

뿐만 아니라 새로운 이미지를 동적으로 추가하게 된다면, 새로운 이미지에 대한 동일한 Event Handler를 addEventListner()에 등록하는 코드가 필요로 하게 된다..;

따라서, 이를 효율적으로 처리하기 위한 방법이 필요한데, 이는 Event Bubbling/Capturing을 이용해서 간단하게 처리할 수 있다.

그럼 Event Bubbling과 Event Capturing 란 무엇일까

Event Bubbling은 특정 Element에서 이벤트가 발생했을때, 자신의 HTML 트리 구조상의 조상 element들에게 이벤트를 전달하는 것을 의미한다.

이와 반대로, Event Capturing은 특정 Element에서 이벤트가 발생했을때, 자신의 HTML 트리 구조상의 자손 element들에게 이벤트를 전달하는 것을 의미한다.

Event Bubbling, Event CapturingaddEventListner() 함수에서 설정한다.
Event Bubbling은 Default 이고, Event Capturing은 옵션 객체로 capture: true를 전달한다.

만약 element.addEventListner() 함수를 통해 동일한 요소에 다른 핸들러를 등록하면 어떻게 될까?
두 핸들러 모두 실행된다.

지금부터의 설명은 Event Bubbling을 가정해 설명하겠다.(Event Bubbling는 반대로 생각하면 된다.)

그렇다면, 상위 Element는 어느 하위 Element에서 이벤트가 발생했는지 알수 있을까?

이러한 경우를 위해서, 핸들러를 통해 전달되는 Event 객체는 currentTargettarget 프로퍼티를 가진다.
구체적으로, currentTarget는 현재 element(즉, 이벤트를 전달받은 상위 요소)를 가리키고, target은 이벤트가 발생한 Element를 가리킨다.

아래 예제를 보자.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<ul>
    <li>
        <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
    </li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click', function(event) {
        console.log(event.currentTarget, event.target);
    });
</script>
</body>
</html>

img 요소에서 클릭이 발생했다고 가정하자. ul 요소에 click 이벤트에 대한 핸들러에서는 아래와 그림같이 현재 currentTargetul 요소와 targetimg 요소를 순차적으로 출력할 것이다.

Event Bubbling, Event Capturing 을 막고 싶을때는 어떻게 할까

Event Bubbling, Event Capturing 를 막고 싶은 경우, 예를 들어 상위 요소로 이벤트를 전달하고 싶지 않은 경우가 있을 것이다. 그러한 경우, event.stopPropagation() 함수를 이용하면 다음 상/하위 요소로의 이벤트를 전파를 막습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<ul>
    <li>
        <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
    </li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click', function(event) {
        console.log(event.currentTarget, event.target);
    });

    const li = document.querySelector('li');
    li.addEventListener('click', function(event) {
        console.log(event.currentTarget, event.target);
        event.stopPropagation();
    });
</script>
</body>
</html>

img 요소에서 클릭이 발생했다고 가정하자. li 요소에서 event.stopPropagation() 로 이벤트 전달을 멈췄기 때문에 상위 요소인 ul에 대한 이벤트 핸들러는 호출되지 않는다.

preventDefault() 함수와 많이 헷갈렸었는데,
stopPropagation()는 상위로의 이벤트 전파만을 방지하는 것이고,
preventDefault()는 브라우저 구현에 의해 처리되는 기존 액션이 동작되지 않도록 이벤트를 취소 함수이다. (ex, a 태그에서 페이지 이동 이벤트를 취소한다. input 입력 취소 ) 즉, 현재 이벤트의 기본 동작을 중지하는 것이다.
결과적으로, stopPropagation()는 이벤트 전파만 막을 뿐이고, 이벤트의 기본 동작을 막지 못한다.
이벤트의 기본 동작을 중단하기 위해서는 preventDefault()를 이용해야만 한다.

그런데 만약에 동일한 Element에 두 개이상의 이벤트 처리기를 리스너로 등록했다면 어떻게 될까?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<ul>
    <li>
        <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
    </li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click', function(event) {
        console.log(event.currentTarget, event.target);
    });

    const li = document.querySelector('li');
    li.addEventListener('click', function(event) {
        console.log('first', event.currentTarget, event.target);
        event.stopPropagation();
    });

    li.addEventListener('click', function(event) {
        console.log('second', event.currentTarget, event.target);
        event.stopPropagation();
    });
</script>
</body>
</html>

위 예제를 보면, li 요소에 두 개의 이벤트 처리기를 등록하였기 때문에, 이벤트 발생시 두 처리기 모두 호출된다.
그런 한 곳에서만 stopPropagation() 함수로 전파를 방지한다고 해서, 두 번째 처리기까지 호출이 무시되는 것은 아니다.

이러한 경우에는 stopImmediatePropagation() 함수를 이용하면 된다.
stopImmediatePropagation()는 같은 이벤트에 대해 상위로의 전파는 물론이고, 등록한 다른 처리기들의 호출까지 막는다.
즉 아래와 같이 동일한 이벤트에 대해 두 개의 처리기를 등록한 경우, 두 번째 처리기의 호출까지 막고 싶다면 첫 번째 처리기에서 stopImmediatePropagation() 함수를 호출해서 이벤트 전파를 중단할 수 있다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<ul>
    <li>
        <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
    </li>
</ul>
<script>
    const ul = document.querySelector('ul');
    ul.addEventListener('click', function(event) {
        console.log(event.currentTarget, event.target);
    });

    const li = document.querySelector('li');
    li.addEventListener('click', function(event) {
        console.log('first', event.currentTarget, event.target);
        event.stopImmediatePropagation();
    });

    li.addEventListener('click', function(event) {
        console.log('second', event.currentTarget, event.target);
    });
</script>
</body>
</html>

click 이벤트에 대한 두 번째 처리기가 호출/처리되지 않는 것을 확인할 수 있다.

Event delegation

이제 Event BubblingEvent Capturing에 대해서 이해했을 것이다.

Event delegation 방식은 Event BubblingEvent Capturing 개념으로 통해서 동작한다.

Event delegation 방식을 이용해서 하위 Element에서 발생한 동일한 이벤트를 상위 요소에서 효율적으로 제어할 수 있습니다.

아래 예제는 처음 보였던 이미지 리스트 예시를 Event delegation 방식으로 효율적으로 구현한 것이다.

기존의 하위 요소에서 각각 등록한 Event Handler를 다르게, 상위 요소에서 한 번에 처리함로써 브라우저는 관리하는 Event Handler 수가 줄어든다. 또한, 리스트에 동적으로 새로운 이미지를 추가하는 경우에 document에 새로운 Event Handler를 등록할 필요가 없어진다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
    <style>
        li {list-style:none;}

        ul > li {
            display:inline-block;
            padding:10px;
            border:1px solid gray;
        }

        .image {
            width:200px;
            height:auto;
        }
    </style>
</head>
<body>
    <ul>
        <li>
            <img src="https://upload.wikimedia.org/wikipedia/commons/d/d6/Horse_2005-08-06_%28Cheval%29.jpg" class="image">
        </li>
        <li>
            <img src="https://image.dongascience.com/Photo/2019/05/3e95c45fbe6710365e999ebbd32ed37e.jpg" class="image">
        </li>
        <li>
            <img src="https://pds.joins.com/news/component/htmlphoto_mmdata/201906/18/f3eb4268-ecf0-40c6-a23a-62dd99d0d5fd.jpg" class="image">
        </li>
        <li>
            <img src="https://img.hani.co.kr/imgdb/resize/2017/1121/00504228_20171121.JPG" class="image">
        </li>
        <li>
            <img src="https://blog.hmgjournal.com/images/contents/article/201603211108-Reissue-pet-family-01.jpg" class="image">
        </li>
    </ul>

    <div></div>

    <script>
        const target = document.querySelector('div');
        const ul = document.querySelector('ul');
        ul.addEventListener('click', function(event) {
            if(event.target.tagName === 'LI') {
                target.innerHTML = "Image url : " + event.target.firstElementChild.src;
            } else if(event.target.tagName === 'IMG'){
                target.innerHTML = "Image url : " + event.target.src ;
            }

        });
    </script>
</body>
</html>

참고자료

HTML templating

HTML templating은 HTML 구조는 유사하고 데이터만 다른 경우에 사용하면 유용하다.

즉, 구조에 해당하는 HTML 양식은 Template으로 만들어두고 외부로 부터 전달받은 데이터를 주입해서 완성된 HTML 구조물을 만드는 방법이다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <ul></ul>
    <script>
        const data = {
            name: 'foo',
            age: 23,
            height: 167
        };

        function make({name, age, height}) {
            return `<li><h4>${name}</h4></li><li><h4>${age}</h4></li><li><h4>${height}</h4></li>`;
        }

        const ul = document.querySelector('ul');
        ul.innerHTML = make(data);
    </script>
</body>
</html>

위 예시에서 make() 함수와 같이, 기본 HTML 구조를 Template으로 만들어 놓고 전달받은 데이터에 따라 다른 html 문자열을 만들어 화면에 추가할 수 있다.

하지만, Javascript 코드안에 HTML을 보관하는 것은 유지보수하기 어렵다.
따라서, 다음과 같이 script 태그에 잘못된 type을 설정하여 Template을 숨겨둘 수 있다.(잘못된 type을 가진 script는 렌더링되지 않는다.)

<script type='text/template'>
	<li><h4>${name}</h4></li><li><h4>${age}</h4></li><li><h4>${height}</h4></li>
</script>

위와 같이, script로 선언한 렌더링되지 않는 Template으로 HTML에 보관하고,replace() 함수를 이용해서 데이터를 주입해 화면에 반영할 수 있다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>

<ul></ul>

<script id='info-template' type='text/template'>
    <li><h4>{name}</h4></li>
    <li><h4>{age}</h4></li>
    <li><h4>{height}</h4></li>
</script>

<script>
    const data = [{
        name: 'foo',
        age: 23,
        height: 167
    }, {
        name: 'bar',
        age: 25,
        height: 176
    }];

    const template = document.querySelector('#info-template').innerHTML;
    const ul = document.querySelector('ul');
    
    data.forEach(item => {
        const html = template.replace('{name}', item.name)
            .replace('{age}', item.age)
            .replace('{height}', item.height);

        ul.innerHTML += html;
    });
</script>
</body>
</html>

참고자료

profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글