부스트코스 강의를 듣고 정리한 내용.

Web UI

서비스 개발을 위한 디렉토리 구성

JS 파일 구성

간단한 내용의 JavaScript라면 한 페이지에 모두 표현하는 것도 좋지만, 그렇지 않다면 의미에 맞게 구분할 것

HTML 안에 JS 파일 구성하기

  • CSS는 head 태그 안에 상단에 위치
    • DOM을 렌더링하기 위해 미리 CSS 파일을 로딩해야 하기 때문
  • JS는 body 태그가 닫히기 전에 소스파일 간 의존성을 이해해 순서대로 배치
    • CSS와 HTML을 이용한 화면의 배치 크기를 렌더링할 때 방해를 하지 않기 위함임

대부분의 JS 코드는 DOM을 찾는 경우가 많다. 예를 들어 document.querySelector()를 사용해 h1 태그를 찾기 등등. 그런데 HTML 코드가 끝나기 전에 위에서 해당 태그에 접근하려 할 경우 null이 뜰 것이다.

브라우저는 한 라인씩 HTML 코드를 파싱하다가 JS 코드가 있으면 실행시킨다. 그런데 document가 구성이 제대로 되지 않았고, 그러니까 당연히 document 하위의 h1 태그를 찾지 못한 것이다.

DOMContentLoaded 이벤트

브라우저가 DOM 트리를 다 그린 시점을 개발자가 알 수 있다면, 그 시점의 DOM 조작이나 DOM에 이벤트 리스너를 붙인이거나 DOM의 어떤 노드를 찾아서 무슨 작업을 한다거나 등의 작업을 에러 없이 할 수 있을 것이다. 그래서 일반적으로는 이 이벤트 발생 이후에 JS 작업을 한다.

load와 DOMContentLoaded 차이 확인

load 이벤트란 모든 웹문서 로드 완료 시 발생하는 이벤트이다. 반면 DOMContentLoaded 이벤트는 초기 HTML 문서를 완전히 불러오고 분석했을 때 발생하는 이벤트로, 스타일 시트, 이미지, 하위 프레임의 로딩은 기다리지 않는다. 이 둘의 차이는 웹사이트에 접속해 크롬 개발자 도구로 확인해볼 수 있다. 개발자도구의 네트워크 패널을 열어 하단에 DOMContentLoaded와 load를 확인해보면 두 개의 시간이 조금 다르다.

DOM Tree 분석이 끝나면 DOMContentLoaded 이벤트가 발생하며, 그 외 모든 자원이 다 받아져서 브라우저에 렌더링(화면 표시)까지 다 끝난 시점에 load가 발생한다.

이를 이해하고, 필요한 시점에 두 개의 이벤트를 사용해서 자바스크립트 실행을 할 수 있다. 보통 DOM tree가 다 만들어지면 DOM APIs를 통해서 DOM에 접근할 수 있기 때문에, 실제로 실무에서는 대부분의 자바스크립트코드는 DOMContentLoaded 이후에 동작하도록 구현한다. 이 방식이 로딩 속도 성능에 유리하다고 생각하기 때문이다.

document.addEventListener("DOMContentLoaded", function() {
  startSomething();
  initFoo();
  initBar();
  var el = document.querySelector("div");
});

Event Delegation

이벤트를 좀 더 효율적으로 등록하는 방법에 대해 공부해보자.

만약 아래 화면처럼 가로로 배치된 책 리스트가 있고, 각각 리스트를 클릭할 때 이벤트가 발생해야 한다고 가정해보자. book list

<ul>
  <li>
    <img src="https://images-na.,,,,,/513hgbYgL._AC_SY400_.jpg" class="product-image" >    </li>
  <li>
    <img src="https://images-n,,,,,/41HoczB2L._AC_SY400_.jpg" class="product-image" >    </li>
  <li>
    <img src="https://images-na.,,,,51AEisFiL._AC_SY400_.jpg" class="product-image" >  </li>
 <li>
    <img src="https://images-na,,,,/51JVpV3ZL._AC_SY400_.jpg" class="product-image" >
 </li>
</ul>

가장 먼저 생각나는 방법은 반복문을 사용하는 것이다. <li> 마다 addEventListener를 통해 이벤트를 등록하면 된다.

var log = document.querySelector(".log");
var lists = document.querySelectorAll("ul > li");

for(var i=0,len=lists.length; i < len; i++) {
  lists[i].addEventListener("click", function(evt) {
     log.innerHTML = "clicked" + evt.currentTarget.firstChild.src;
  });
}

이벤트 큐에 각각의 리스트에 대한 이벤트 리스너가 맵 구조로 들어가게 된다.

그러나 이 방식의 경우 리스트의 길이가 길어지면 그만큼 브라우저가 기억해야 할 이벤트 리스너도 많아진다. 그리고 만약 list에 새 책이 동적으로 추가될 경우 추가된 요소에 대해서도 addEventListener를 해줘야 하는 불편함이 있다.

target 정보를 사용하면 이를 개선할 수 있다. 이번에는 <ul> 태그에만 이벤트를 새롭게 등록해보자.

ul.addEventListener("click",function(evt) {
    console.log(evt.currentTarget, evt.target);
});

이 경우에도 <li> 안의 이미지를 클릭하면 리스너가 실행된다. 이는 클릭한 지점이 하위 엘리먼트라 해도 그것을 감싸고 있는 상위 엘리먼트까지 올라가면서 이벤트 리스너가 있는지 찾는 이벤트 버블링(Bubbling)이라는 현상 때문이다.

이벤트 버블링: 한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작하는 것. 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작한다.

반대로 이벤트 캡처링(Capturing)이라는 것도 있다. 반대로 상위 엘리먼트부터 이벤트가 발생하는 것인데, 잘 쓰이지는 않는다. bubbling은 target 요소에서 최상위 요소까지 이벤트를 전파하고, Capturing은 최상위 요소에서 target 요소까지 이벤트를 전파한다. 만약 캡처링 단계에서 이벤트 발생을 시키고 싶다면 addEventListener 메소드의 세 번째 인자 값으로 true를 주면 된다.

event bubbling

버블링으로 인하여 <img><li>를 클릭해도 <ul>에 등록된 이벤트 리스너가 실행된다는 것을 알았다. 그렇지만 <img>를 클릭했으므로 target은 여전히 <img>이다. target 정보는 실제 클릭된 하위 엘리먼트를 알려주기 때문이다. 이 점을 이용해 src를 추출할 수 있지 않을까?

var ul = document.querySelector("ul");
ul.addEventListener("click",function(evt) {
    if(evt.target.tagName === "IMG") {
      log.innerHTML = "clicked" + evt.target.src;
    }
});

이제 addEventListener를 한 번만 쓰면서 모든 리스트의 이미지 정보를 확인할 수 있고, <li> 태그가 하나 더 추가돼도 문제없이 작동하는 것처럼 보인다.

그러나 문제가 있다. 각 사진 사이에는 패딩이 존재하는데, 이는 <li>의 영역이다. 이 부분을 클릭하면 tagName이 <li>이기 때문에, 공백을 클릭해도 이미지 url을 출력하기 위해서는 tagName이 <li>인 경우도 고려해야 한다.

var ul = document.querySelector("ul");
ul.addEventListener("click",function(evt) {
  debugger;
    if(evt.target.tagName === "IMG") {
      log.innerHTML = "clicked" + evt.target.src;
    } else if (evt.target.tagName === "LI") {
      log.innerHTML = "clicked" + evt.target.firstChild.src;
    }
});

이미지 태그에 발생해야 할 이벤트를 위쪽 부모에게 위임한다고 해서 이를 event delegation이라 한다. event delegation을 하면 자식 노드에 각각 이벤트 등록을 하지 않고도 효율적으로 이벤트를 등록할 수 있다.

Bubbling and Capturing

HTML Templating

HTML Templating

templating이란 HTML과 데이터를 섞어서 웹 화면에 어떤 변경을 주는 것을 말한다.

아래 화면에서 JSON 형태의 데이터를 Ajax로 받아와 화면에 추가해야 한다고 생각해보자.

product list

위 리스트들은 구조는 거의 동일하나 데이터의 값들만 다른 것이다.

반복적인 HTML부분을 template로 만들어두고, 서버에서 온 데이터(주로JSON)을 결합해서, 화면에 추가하는 작업이라고 할 수 있다.

보통 템플릿 작업은 클라이언트에서만 할 수 있는 것은 아니다. 백엔드에서 데이터를 조회한 다음에 그 내용들을 동적으로 HTML로 만들어 클라이언트에 보내줄 수도 있음.

결합 과정 해결하기

간단한 코드

var data = {  title : "hello",
              content : "lorem dkfief",
              price : 2000
           };
var html = "<li><h4>{title}</h4><p>{content}</p><div>{price}</div></li>";

// replace()는 메소드 체이닝 가능
html.replace("{title}", data.title)
    .replace("{content}", data.content)
    .replace("{price}", data.price)

HTML Templating 실습

HTML Template의 보관

HTML 문자열을 어딘가 보관해야 하는데, JS 코드 안에서 이런 정적인 데이터를 보관하는 것은 좋지 않다.

var html = "<li><h4>{title}</h4><p>{content}</p><div>{price}</div></li>";

여러 가지 방법이 있다.

  1. 서버에서 파일로 보관하고 Ajax로 요청해 받아온다.
  2. HTML 코드 안에 숨겨둔다.

우리는 데이터의 양이 많지 않으므로 HTML 안에 보관해보자.

Templating

HTML에서 <script> 태그는 타입이 JS가 아니라면 렌더링하지 않고 무시한다.

<script id="template-list-item" type="text/template">
  <li>
      <h4>{title}</h4><p>{content}</p><div>{price}</div>
  </li>
</script>

이 점을 이용하면 querySelector()로 가져올 수 있다.

var html = document.querySelector("template-list-item");
var data = [
        {title : "hello",content : "lorem dkfief",price : 2000},
        {title : "hello",content : "lorem dkfief",price : 2000}
];

//html 에 script에서 가져온 html template.
var html = document.querySelector("#template-list-item").innerHTML;

var resultHTML = "";

for(var i=0; i<data.length; i++) {
    resultHTML += html.replace("{title}", data[i].title)
                      .replace("{content}", data[i].content)
                      .replace("{price}", data[i].price);
}

document.querySelector(".content").innerHTML = resultHTML;

TAB UI 실습

tabui.html

<html>
  <head>
    <link rel="stylesheet" href="./tabui.css" />
  </head>
  <body>
    <h2>TAB UI TEST</h2>
    <div class="tab">
      <div class="tabmenu">
        <div>alnim</div>
        <div>snoa</div>
        <div>boonhong</div>
        <div>soongomi</div>
      </div>
      <section class="content">
        <h4>hello alnim!</h4>
        <p>perfume, clothes</p>
      </section>
    </div>
    <script>
      function makeTemplate(data, clickedName) {
        var html = document.getElementById("tabcontent").innerHTML;
        var resultHTML = "";

        for (var i = 0; i < data.length; i++) {
          if (data[i].name === clickedName) {
            resultHTML = html
              .replace("{name}", data[i].name)
              .replace("{favourites}", data[i].favourites.join(" "));
            break;
          }
        }
        document.querySelector(".content").innerHTML = resultHTML;
      }

      function sendAjax(url, clickedName) {
        var oReq = new XMLHttpRequest();
        oReq.addEventListener("load", () => {
          var data = JSON.parse(oReq, responseText);
          makeTemplate(data, clickedName);
        });
        oReq.open("GET", url);
        oReq.send();
      }

      var tabmenu = document.querySelector(".tabmenu");
      tabmenu.addEventListener("click", (e) => {
        sendAjax("./json.txt", e.target.innerText);
      });
    </script>
    <script id="tabcontent" type="my-template">
      <h4>hello {name}!</h4>
      <p>{favourites}</p>
    </script>
  </body>
</html>

tabui.css

body {
  font-family: sans-serif;
}
h2 {
  text-align: center;
}
h2,
h4 {
  margin: 0px;
}
.tab {
  width: 600px;
  margin: 0px auto;
}
.tabmenu {
  background-color: bisque;
}
.tabmenu > div {
  display: inline-block;
  width: 140px;
  margin: 0px;
  text-align: center;
  height: 50px;
  line-height: 50px;
  cursor: pointer;
}
.content {
  padding: 5%;
  background-color: antiquewhite;
}

json.txt

[
    {
        "name": "alnim",
        "favourites": ["perfume", "clothes"]
    },
    {
        "name": "snoa",
        "favourites": ["alnim", "sleep"]
    },
    {
        "name": "boonhong",
        "favourites": ["pink", "nap", "snoa"]
    },
    {
        "name": "soongom",
        "favourites": ["hello", "alnim"]
    }
]