6일 5월 2020

브라우저 이벤트 소개

이벤트(event) 는 무언가 일어났다는 신호입니다. 모든 DOM 노드는 이런 신호를 만들어 냅니다. 참고로, 이벤트는 DOM에만 한정되진 않습니다.

자주 사용되는 유용한 DOM 이벤트는 무엇이 있는지 잠시 살펴봅시다.

마우스 이벤트:

  • click – 요소 위에서 마우스 왼쪽 버튼을 눌렀을 때(터치스크린이 있는 장치에선 탭 했을 때) 발생합니다.
  • contextmenu – 요소 위에서 마우스 오른쪽 버튼을 눌렀을 때 발생합니다.
  • mouseovermouseout – 마우스 커서를 요소 위로 움직였을 때, 커서가 요소 밖으로 움직였을 때 발생합니다.
  • mousedownmouseup – 요소 위에서 마우스 왼쪽 버튼을 누르고 있을 때, 마우스 버튼을 뗄 때 발생합니다.
  • mousemove – 마우스를 움직일 때 발생합니다.

폼 요소 이벤트:

  • submit – 사용자가 <form>을 제출할 때 발생합니다.
  • focus – 사용자가 <input>과 같은 요소에 포커스 할 때 발생합니다.

키보드 이벤트:

  • keydownkeyup – 사용자가 키보드 버튼을 누르거나 뗄 때 발생합니다.

문서 이벤트:

  • DOMContentLoaded – HTML이 전부 로드 및 처리되어 DOM 생성이 완료되었을 때 발생합니다.

CSS 이벤트:

  • transitionend – CSS 애니메이션이 종료되었을 때 발생합니다.

이 외에도 다양한 이벤트가 있는데, 몇몇 이벤트는 다음 챕터에서 자세히 다룰 예정입니다.

이벤트 핸들러

이벤트에 반응하려면 이벤트가 발생했을 때 실행되는 함수인 핸들러(handler) 를 할당해야 합니다.

핸들러는 사용자의 행동에 어떻게 반응할지를 자바스크립트 코드로 표현한 것입니다.

핸들러는 여러 가지 방법으로 할당할 수 있습니다. 가장 간단한 방법부터 살펴봅시다.

HTML 속성

HTML 안의 on<event> 속성에 핸들러를 할당할 수 있습니다.

아래 같이 input 태그의 onclick 속성에 click 핸들러를 할당하는 것 같이 말이죠.

<input value="클릭해 주세요." onclick="alert('클릭!')" type="button">

버튼을 클릭하면 onclick 안의 코드가 실행됩니다.

여기서 주의해야 할 것은 속성값 내에서 사용된 따옴표입니다. 속성값 전체가 큰따옴표로 둘러싸여 있기 때문에 작은 따옴표로 둘러쌓습니다. onclick="alert("클릭!")"과 같이 속성값 내부에 또 큰따옴표를 쓰면 코드가 작동하지 않습니다.

긴 코드를 HTML 속성값으로 사용하는 것은 추천하지 않습니다. 만약 코드가 길다면, 함수를 만들어서 이를 호출하는 방법을 추천합니다.

아래 버튼을 클릭하면 함수 countRabbits()이 호출됩니다.

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert(`토끼 ${i}마리`);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="토끼를 세봅시다!">

HTML 속성은 대·소문자를 구분하지 않기 때문에, ONCLICKonClick이나 onCLICK과 동일하게 작동합니다. 하지만 속성값은 대개 onclick 같이 소문자로 작성합니다.

DOM 프로퍼티

DOM 프로퍼티 on<event>을 사용해도 핸들러를 할당할 수 있습니다.

elem.onclick을 사용한 예시:

<input id="elem" type="button" value="클릭해 주세요.">
<script>
  elem.onclick = function() {
    alert('감사합니다.');
  };
</script>

핸들러를 HTML 속성을 사용해 할당하면, 브라우저는 속성값을 이용해 새로운 함수를 만듭니다. 그리고 생성된 함수를 DOM 프로퍼티에 할당합니다.

따라서 DOM 프로퍼티를 사용해 핸들러를 만든 위 예시는 HTML 속성을 사용해 만든 바로 위쪽 예시와 동일하게 작동합니다.

핸들러는 언제나 DOM 프로퍼티에 할당됩니다. HTML 속성을 사용해 핸들러를 정의하는 방법은 DOM 프로퍼티를 초기화하는 여러 방법의 하나일 뿐입니다.

아래 두 예시는 동일하게 작동합니다.

  1. HTML만 사용하는 방법

    <input type="button" onclick="alert('클릭!')" value="클릭해 주세요.">
  2. HTML과 자바스크립트를 함께 사용하는 방법

    <input type="button" id="button" value="클릭해 주세요.">
    <script>
      button.onclick = function() {
        alert('클릭!');
      };
    </script>

onclick 프로퍼티는 단 하나밖에 없기 때문에, 복수의 이벤트 핸들러를 할당할 수 없습니다.

아래 예시와 같이 핸들러를 하나 더 추가하면, 기존 핸들러는 덮어씌워 집니다.

<input type="button" id="elem" onclick="alert('이전')" value="클릭해 주세요.">
<script>
  elem.onclick = function() { // 기존에 작성된 핸들러를 덮어씀
    alert('이후'); // 이 경고창만 보입니다.
  };
</script>

이미 존재하는 함수를 직접 핸들러에 할당할 수도 있습니다.

function sayThanks() {
  alert('감사합니다!');
}

elem.onclick = sayThanks;

핸들러를 제거하고 싶다면 elem.onclick = null 같이 null을 할당하면 됩니다.

this로 요소에 접근하기

핸들러 내부에 쓰인 this의 값은 핸들러가 할당된 요소입니다.

아래 예시의this.innerHTML에서 this는 button이므로 버튼을 클릭하면 버튼 안의 콘텐츠가 얼럿창에 출력됩니다.

<button onclick="alert(this.innerHTML)">클릭해 주세요.</button>

자주 하는 실수

이벤트를 다룰 때는 아래 주의사항을 항상 염두에 두시기 바랍니다.

함수는 sayThanks처럼 할당해야 합니다. sayThanks()를 할당하면 동작하지 않습니다.

// 올바른 방법
button.onclick = sayThanks;

// 틀린 방법
button.onclick = sayThanks();

sayThanks() 같이 괄호를 덧붙이는 것은 함수를 호출하겠다는 것을 의미합니다. 위 예시의 마지막 줄처럼 sayThanks()를 프로퍼티에 할당하면 함수 호출의 결괏(result)값이 할당되죠. 함수 sayThanks가 아무것도 반환하지 않는다면 onclick 프로퍼티엔 undefined이 할당되므로 이벤트가 원하는 대로 동작하지 않습니다.

그런데, HTML 속성값에는 괄호가 있어야 합니다.

<input type="button" id="button" onclick="sayThanks()">

브라우저는 속성값을 읽고, 이 값을 함수 본문으로 하는 핸들러 함수를 만들기 때문에 이런 차이가 발생합니다.

브라우저는 onclick 프로퍼티에 새로운 함수를 할당하죠.

button.onclick = function() {
  sayThanks(); // 속성값
};

문자열이 아닌 함수를 쓰세요.

elem.onclick = "alert(1)"도 잘 작동하긴 합니다. 호환성 유지를 위해 문자열을 프로퍼티에 할당해도 문제가 없게 만들어 놓았기 때문이죠. 하지만 이 방법은 쓰지 않으시길 강력히 권유합니다.

setAttribute로 핸들러를 할당하지 마세요.

아래 코드는 동작하지 않습니다.

// <body>를 클릭하면 에러가 발생합니다.
// 속성은 항상 문자열이기 때문에, 함수가 문자열이 되어버리기 때문입니다.
document.body.setAttribute('onclick', function() { alert(1) });

DOM 프로퍼티는 대·소문자를 구분합니다.

핸들러 할당 시 elem.onclick은 괜찮지만, elem.ONCLICK은 안됩니다. DOM 프로퍼티는 대·소문자를 구분하기 때문입니다.

addEventListener

HTML 속성과 DOM 프로퍼티를 이용한 이벤트 핸들러 할당 방식엔 근본적인 문제가 있습니다. 하나의 이벤트에 복수의 핸들러를 할당할 수 없다는 문제이죠.

버튼을 클릭하면 버튼을 강조하면서 메시지를 보여주고 싶다고 해 봅시다.

두 개의 이벤트 핸들러가 필요할 겁니다. 하지만 기존 방법으로는 프로퍼티가 덮어씌워 진다는 문제가 있습니다.

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // 이전 핸들러를 덮어씀

웹 표준에 관여하는 개발자들은 오래전부터 이 문제를 인지하고, addEventListenerremoveEventListener 라는 특별한 메서드를 이용해 핸들러를 관리하자는 대안을 제시했습니다. 핸들러를 여러 개 할당할 수 있도록 말이죠.

문법은 다음과 같습니다.

element.addEventListener(event, handler, [options]);
event
이벤트 이름(예: "click")
handler
핸들러 함수
options
아래 프로퍼티를 갖는 객체
  • once: true이면 이벤트가 트리거 될 때 리스너가 자동으로 삭제됩니다.
  • capture: 어느 단계에서 이벤트를 다뤄야 하는지를 알려주는 프로퍼티로, 관련 내용은 버블링과 캡처링 챕터에서 자세히 다룰 예정입니다. 호환성 유지를 위해 options를 객체가 아닌 false/true로 할당하는 것도 가능한데, 이는 {capture: false/true}는 와 동일합니다.
  • passive: true이면 리스너에서 지정한 함수가 preventDefault()를 호출하지 않습니다. 브라우저 기본 동작 챕터에서 자세히 다루겠습니다.

핸들러 삭제는 removeEventListener로 합니다.

element.removeEventListener(event, handler, [options]);
삭제는 동일한 함수만 할 수 있습니다.

핸들러를 삭제하려면 핸들러 할당 시 사용한 함수를 그대로 전달해주어야 합니다.

아래와 같이 이벤트를 할당하고 삭제하면 원하는 대로 동작하지 않습니다.

elem.addEventListener( "click" , () => alert('감사합니다!'));
// ....
elem.removeEventListener( "click", () => alert('감사합니다!'));

removeEventListener를 썼지만, 핸들러는 지워지지 않습니다. removeEventListeneraddEventListener를 사용해 할당한 함수와 다른 함수를 받고 있기 때문입니다. 함수는 똑같게 생겼지만 그럼에도 불구하고 다른 함수이기 때문에 이런 문제가 발생합니다.

위 예시를 제대로 고치면 다음과 같습니다.

function handler() {
  alert( '감사합니다!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

변수에 핸들러 함수를 저장해 놓지 않으면 핸들러를 지울 수 없다는 것을 항상 기억해 놓으셔야 합니다. 이렇게 하지 않으면 addEventListener로 할당한 핸들러를 ‘불러올’ 수 없습니다.

addEventListener를 여러 번 호출하면 아래와 같이 핸들러를 여러 개 붙일 수 있습니다.

<input id="elem" type="button" value="클릭해 주세요."/>

<script>
  function handler1() {
    alert('감사합니다!');
  };

  function handler2() {
    alert('다시 한번 감사합니다!');
  }

  elem.onclick = () => alert("안녕하세요.");
  elem.addEventListener("click", handler1); // 감사합니다!
  elem.addEventListener("click", handler2); // 다시 한번 감사합니다!
</script>

지금까지 살펴본 바와 같이 핸들러는 DOM 프로퍼티와 addEventListener 를 사용하는 방법 두 가지를 사용해 할당할 수 있습니다. 대개는 두 방법 중 하나만을 사용해 할당합니다.

어떤 이벤트는 addEventListener를 써야만 동작합니다.

DOM 프로퍼티에 할당할 수 없는 이벤트가 몇몇 있습니다. 이런 이벤트는 무조건 addEventListener를 써야 합니다.

문서를 읽고 DOM 트리 생성이 완료되었을 때 트리거되는 이벤트인 DOMContentLoaded가 대표적인 예입니다.

document.onDOMContentLoaded = function() {
  alert("DOM이 완성되었습니다."); // 이 얼럿창은 절대 뜨지 않습니다.
};
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM이 완성되었습니다."); // 이 얼럿창은 제대로 뜹니다.
});

이처럼 addEventListener는 좀 더 범용적입니다. addEventListener를 써야만 동작하는 이벤트들은 예외적인 경우라고 생각하시면 될 것 같습니다.

이벤트 객체

이벤트를 제대로 다루려면 어떤 일이 일어났는지 상세히 알아야 합니다. ‘click’ 이벤트가 발생했다면 마우스 포인터가 어디에 있는지, ‘keypress’ 이벤트가 발생했다면 어떤 키가 눌렸는지 등에 대한 상세한 정보가 필요합니다.

이벤트가 발생하면 브라우저는 *이벤트 객체(event object)*라는 것을 만듭니다. 여기에 이벤트에 관한 상세한 정보를 넣은 다음, 핸들러에 인수 형태로 전달합니다.

아래는 이벤트 객체로부터 마우스 좌표 정보를 얻어내는 예시입니다.

<input type="button" value="클릭해 주세요." id="elem">

<script>
  elem.onclick = function(event) {
    // 이벤트 타입과 요소, 클릭 이벤트가 발생한 좌표를 보여줌
    alert(event.type + " 이벤트가 " + event.currentTarget + "에서 발생했습니다.");
    alert("이벤트가 발생한 곳의 좌표는 " + event.clientX + ":" + event.clientY +"입니다.");
  };
</script>

이벤트 객체에서 지원하는 프로퍼티 중 일부는 다음과 같습니다.

event.type
이벤트 타입, 위 예시에선 "click".
event.currentTarget
이벤트를 처리하는 요소. 화살표 함수를 사용해 핸들러를 만들거나 다른 곳에 바인딩하지 않은 경우엔 this가 가리키는 값과 같음, 화살표 함수를 사용했거나 함수를 다른 곳에 바인딩한 경우엔 event.currentTarget를 사용해 이벤트가 처리되는 요소 정보를 얻을 수 있음
event.clientX / event.clientY
마우스 관련 이벤트에서, 커서의 상대 좌표(모니터 기준 좌표가 아닌, 브라우저 화면 기준 좌표 – 옮긴이)

이 외에도 다양한 프로퍼티가 있습니다. 이벤트 타입에 따라 이벤트 객체에서 제공하는 프로퍼티는 다릅니다. 추후 다양한 종류의 이벤트를 학습하면서 이벤트별 프로퍼티에 대해서도 상세히 알아보겠습니다.

이벤트 객체는 HTML에서도 접근할 수 있습니다.

HTML에서 핸들러를 할당한 경우에도 아래와 같이 event 객체를 사용할 수 있습니다.

<input type="button" onclick="alert(event.type)" value="이벤트 타입">

브라우저는 속성을 읽고 function(event) { alert(event.type) } 같은 핸들러를 만들어 내기 때문입니다. 생성된 핸들러 함수의 첫 번째 인수는 "event"로 불리고, 함수 본문은 속성값 가져옵니다.

객체 형태의 핸들러와 handleEvent

addEventListener를 사용하면 함수뿐만 아니라 객체를 이벤트 핸들러로 할당할 수 있습니다. 이벤트가 발생하면 객체에 구현한 handleEvent 메서드가 호출됩니다.

예시:

<button id="elem">클릭해 주세요.</button>

<script>
  elem.addEventListener('click', {
    handleEvent(event) {
      alert(event.type + " 이벤트가 " + event.currentTarget + "에서 발생했습니다.");
    }
  });
</script>

보시다시피 addEventListener가 인수로 객체 형태의 핸들러를 받으면 이벤트 발생 시 object.handleEvent(event)가 호출됩니다.

클래스를 사용할 수도 있습니다.

<button id="elem">클릭해 주세요.</button>

<script>
  class Menu {
    handleEvent(event) {
      switch(event.type) {
        case 'mousedown':
          elem.innerHTML = "마우스 버튼을 눌렀습니다.";
          break;
        case 'mouseup':
          elem.innerHTML += " 그리고 버튼을 뗐습니다.";
          break;
      }
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

위 예시에선 하나의 객체에서 두 개의 이벤트를 처리하고 있습니다. 이때 주의할 점은 addEventListener를 사용할 때는 요소에 타입을 정확히 명시해 주어야 한다는 점입니다. 위 예시에서 menu 객체는 오직 mousedownmouseup이벤트에만 응답하고, 다른 타입의 이벤트에는 응답하지 않습니다.

handleEvent 메서드가 모든 이벤트를 처리할 필요는 없습니다. 이벤트 관련 메서드를 handleEvent 에서 호출해서 사용할 수도 있습니다. 아래와 같이 말이죠,

<button id="elem">클릭해 주세요.</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "마우스 버튼을 눌렀습니다.";
    }

    onMouseup() {
      elem.innerHTML += " 그리고 버튼을 뗐습니다.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

이벤트 핸들러가 명확히 분리되었기 때문에 코드 변경이 원활해졌습니다.

요약

이벤트 핸들러는 3가지 방법으로 할당할 수 있습니다.

  1. HTML 속성: onclick="...".
  2. DOM 프로퍼티: elem.onclick = function.
  3. 메서드: elem.addEventListener(event, handler[, phase])로 핸들러를 추가하고, removeEventListener 로 핸들러를 제거함

HTML 속성을 이용한 이벤트 핸들러 할당은 자주 쓰이지 않습니다. HTML 태그 중간에 자바스크립트가 들어가 있으면 어색하기 때문입니다. 긴 코드를 끼워 넣는 게 불가능한 점도 이유 중 하나입니다.

DOM 프로퍼티를 사용한 방법은 괜찮습니다. 하지만 복수의 핸들러 할당이 불가능하다는 단점이 있습니다. 여러 상황에서 이런 제약이 큰 단점이 되진 않지만요.

메서드를 사용하는 방법은 가장 유연하지만, 코드는 가장 깁니다. transitionendDOMContentLoaded(추후 다룰 예정)같은 일부 이벤트는 이 방법으로만 처리할 수 있습니다. addEventListener는 객체 형태의 이벤트를 지원합니다. 이 경우엔 이벤트 발생 시 객체 안에 구현된 메서드인 handleEvent가 호출됩니다.

어떤 방법으로 이벤트 핸들러를 할당하던, 첫 번째 인자는 이벤트 객체입니다. 이벤트 객체는 어떤 일이 일어났는지에 대한 상세한 정보를 담고 있습니다.

다음 주제에서 이벤트에 대해 전반적인 내용과 다양한 이벤트 타입에 대해서 다루겠습니다.

과제

중요도: 5

Add JavaScript to the button to make <div id="text"> disappear when we click it.

The demo:

샌드박스를 열어 정답을 작성해보세요.

중요도: 5

Create a button that hides itself on click.

Like this:

Can use this in the handler to reference “the element itself” here:

<input type="button" onclick="this.hidden=true" value="Click to hide">
중요도: 5

There’s a button in the variable. There are no handlers on it.

Which handlers run on click after the following code? Which alerts show up?

button.addEventListener("click", () => alert("1"));

button.removeEventListener("click", () => alert("1"));

button.onclick = () => alert(2);

The answer: 1 and 2.

The first handler triggers, because it’s not removed by removeEventListener. To remove the handler we need to pass exactly the function that was assigned. And in the code a new function is passed, that looks the same, but is still another function.

To remove a function object, we need to store a reference to it, like this:

function handler() {
  alert(1);
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

The handler button.onclick works independently and in addition to addEventListener.

중요도: 5

Move the ball across the field to a click. Like this:

Requirements:

  • The ball center should come exactly under the pointer on click (if possible without crossing the field edge).
  • CSS-animation is welcome.
  • The ball must not cross field boundaries.
  • When the page is scrolled, nothing should break.

Notes:

  • The code should also work with different ball and field sizes, not be bound to any fixed values.
  • Use properties event.clientX/event.clientY for click coordinates.

샌드박스를 열어 정답을 작성해보세요.

First we need to choose a method of positioning the ball.

We can’t use position:fixed for it, because scrolling the page would move the ball from the field.

So we should use position:absolute and, to make the positioning really solid, make field itself positioned.

Then the ball will be positioned relatively to the field:

#field {
  width: 200px;
  height: 150px;
  position: relative;
}

#ball {
  position: absolute;
  left: 0; /* relative to the closest positioned ancestor (field) */
  top: 0;
  transition: 1s all; /* CSS animation for left/top makes the ball fly */
}

Next we need to assign the correct ball.style.left/top. They contain field-relative coordinates now.

Here’s the picture:

We have event.clientX/clientY – window-relative coordinates of the click.

To get field-relative left coordinate of the click, we can substract the field left edge and the border width:

let left = event.clientX - fieldCoords.left - field.clientLeft;

Normally, ball.style.left means the “left edge of the element” (the ball). So if we assign that left, then the ball edge, not center, would be under the mouse cursor.

We need to move the ball half-width left and half-height up to make it center.

So the final left would be:

let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2;

The vertical coordinate is calculated using the same logic.

Please note that the ball width/height must be known at the time we access ball.offsetWidth. Should be specified in HTML or CSS.

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

Create a menu that opens/collapses on click:

P.S. HTML/CSS of the source document is to be modified.

샌드박스를 열어 정답을 작성해보세요.

HTML/CSS

First let’s create HTML/CSS.

A menu is a standalone graphical component on the page, so it’s better to put it into a single DOM element.

A list of menu items can be laid out as a list ul/li.

Here’s the example structure:

<div class="menu">
  <span class="title">Sweeties (click me)!</span>
  <ul>
    <li>Cake</li>
    <li>Donut</li>
    <li>Honey</li>
  </ul>
</div>

We use <span> for the title, because <div> has an implicit display:block on it, and it will occupy 100% of the horizontal width.

Like this:

<div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div>

So if we set onclick on it, then it will catch clicks to the right of the text.

As <span> has an implicit display: inline, it occupies exactly enough place to fit all the text:

<span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span>

Toggling the menu

Toggling the menu should change the arrow and show/hide the menu list.

All these changes are perfectly handled by CSS. In JavaScript we should label the current state of the menu by adding/removing the class .open.

Without it, the menu will be closed:

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

…And with .open the arrow changes and the list shows up:

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

샌드박스를 열어 정답을 확인해보세요.

중요도: 5

There’s a list of messages.

Use JavaScript to add a closing button to the right-upper corner of each message.

The result should look like this:

샌드박스를 열어 정답을 작성해보세요.

To add the button we can use either position:absolute (and make the pane position:relative) or float:right. The float:right has the benefit that the button never overlaps the text, but position:absolute gives more freedom. So the choice is yours.

Then for each pane the code can be like:

pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>');

Then the <button> becomes pane.firstChild, so we can add a handler to it like this:

pane.firstChild.onclick = () => pane.remove();

샌드박스를 열어 정답을 확인해보세요.

중요도: 4

Create a “carousel” – a ribbon of images that can be scrolled by clicking on arrows.

Later we can add more features to it: infinite scrolling, dynamic loading etc.

P.S. For this task HTML/CSS structure is actually 90% of the solution.

샌드박스를 열어 정답을 작성해보세요.

The images ribbon can be represented as ul/li list of images <img>.

Normally, such a ribbon is wide, but we put a fixed-size <div> around to “cut” it, so that only a part of the ribbon is visible:

To make the list show horizontally we need to apply correct CSS properties for <li>, like display: inline-block.

For <img> we should also adjust display, because by default it’s inline. There’s extra space reserved under inline elements for “letter tails”, so we can use display:block to remove it.

To do the scrolling, we can shift <ul>. There are many ways to do it, for instance by changing margin-left or (better performance) use transform: translateX():

The outer <div> has a fixed width, so “extra” images are cut.

The whole carousel is a self-contained “graphical component” on the page, so we’d better wrap it into a single <div class="carousel"> and style things inside it.

샌드박스를 열어 정답을 확인해보세요.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.