부스트코스 강의를 듣고 정리한 내용.
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
이벤트를 좀 더 효율적으로 등록하는 방법에 대해 공부해보자.
만약 아래 화면처럼 가로로 배치된 책 리스트가 있고, 각각 리스트를 클릭할 때 이벤트가 발생해야 한다고 가정해보자.
<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를 주면 된다.
버블링으로 인하여 <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을 하면 자식 노드에 각각 이벤트 등록을 하지 않고도 효율적으로 이벤트를 등록할 수 있다.
HTML Templating
HTML Templating
templating이란 HTML과 데이터를 섞어서 웹 화면에 어떤 변경을 주는 것을 말한다.
아래 화면에서 JSON 형태의 데이터를 Ajax로 받아와 화면에 추가해야 한다고 생각해보자.
위 리스트들은 구조는 거의 동일하나 데이터의 값들만 다른 것이다.
반복적인 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>";
여러 가지 방법이 있다.
- 서버에서 파일로 보관하고 Ajax로 요청해 받아온다.
- 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"]
}
]