* 이벤트의 개념
이벤트는 특정 사건이나 행동을 의미한다. 브라우저에서의 이벤트는 사용자가 버튼을 클릭한다든지 입력창에 뭔가를 입력하는 등의 사건을 가리킨다.
* 이벤트핸들러 함수
사용자가 버튼을 클릭한더던지 입력창에 뭔가를 입력했을때 이를 처리하는 로직을 이벤트 핸들러 함수라고 한다.
* 이벤트핸들러와 이벤트루프 (비동기 처리)
이벤트핸들러 함수는 자바스크립트 엔진에 의하여 이벤트루프에 등록되었다가 프로그램 메인에 있는 전체코드가 모두 실행된 이후에 사용자가 특정 이벤트를 발생시키면 실행이 된다. 이를 프로그래밍 용어로 비동기 처리라고 한다. 비동기 처리는 함수를 등록만 해놓고 나중에 실행하는 것을 의미한다.
* 이벤트핸들러 함수를 연결하는 세가지 방법
인라인 방식
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<button onclick='popup()'>팝업창 열기</button>
<script src='app.js'></script>
</body>
</html>
예제코드에 사용된 HTML 파일은 위와 같다. 버튼을 클릭했을때 경고창이 열리는 간단한 예제이다. button 요소 내부에 onclick 이라는 속성을 설정하고 popup 이라는 이벤트 핸들러 함수를 연결하였다. 이렇게 하면 버튼을 클릭했을때 popup 이라는 이벤트 핸들러 함수가 실행이 된다.
function popup(){
alert('팝업창')
}
자바스크립트 코드는 위와 같다. popup 이라는 함수가 정의되어 있다. popup 은 이벤트 핸들러 함수이다. 사용자가 버튼을 클릭하면 실행이 된다. alert 함수는 브라우저에서 경고창을 띄워주는 브라우저 API 이다.
하지만 인라인 방식은 되도록이면 사용하지 않는 것이 좋다. 우선 해킹에 취약하다. 해커가 HTML 파일에서 인라인 방식으로 설정된 이벤트핸들러 함수를 찾아서 프로그래밍적으로 변경하게 되면 웹사이트가 이상한 동작을 하게 된다. 또한, 무한 반복적인 클릭과 같은 동작을 하게 하면 웹사이트 자체를 마비시킬수도 있다.
두번째 이유는 유지보수를 힘들게 한다. 이는 HTML 코드와 자바스크립트 코드가 섞여 있기 때문이다. 항상 서로 다른 로직은 분리하는게 유지보수와 가독성에 도움이 된다.
요소의 프로퍼티 방식
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<button id='popup'>팝업창 열기</button>
<script src='app.js'></script>
</body>
</html>
예제코드에 사용된 HTML 파일은 위와 같다. 인라인 방식과 다르게 버튼에 id 값만 설정한다.
const popup = document.getElementById('popup')
popup.onclick = function (){
alert('팝업창')
}
id 값으로 버튼 요소를 검색한다. 해당 요소의 onclick 이라는 프로퍼티에 이벤트핸들러 함수를 설정한다. 이렇게 해도 인라인 방식과 동일하게 버튼을 클릭할때마다 경고창이 뜬다.
요소의 메서드 방식
요소.addEventListener('이벤트 이름', 이벤트핸들러 함수)
요소의 addEventListener 라는 메서드를 사용하는 방식이다. 가장 일반적이고 자주 사용된다. addEventListener 의 첫번째 인자로 'click' 'change' 와 같은 이벤트 이름을 전달하고, 두번째 인자에는 이벤트가 발생했을때 실행할 이벤트핸들러 함수를 설정해주면 된다.
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', function (){
alert('팝업창')
})
위 코드는 id 값으로 popup 이라는 버튼 요소를 검색한 다음 addEventListener 메서드를 사용하여 이벤트핸들러를 등록한다. 이벤트핸들러 함수는 이름이 없는데 이를 익명함수라고 한다.
function popup(){
alert('팝업창')
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
위 코드는 addEventListener 메서드 내부에 놓여있었던 이벤트 핸들러 함수를 외부로 이동하였다. 또한, 이벤트핸들러 함수에 popup 이라는 함수명이 존재한다.
function popup(){
alert('팝업창')
}
function setBtnColor(){
popupBtn.style.background = 'skyblue'
}
function unsetBtnColor(){
popupBtn.style.background = ''
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
popupBtn.addEventListener('mouseover', setBtnColor)
popupBtn.addEventListener('mouseout', unsetBtnColor)
addEventListener 메서드를 사용하는 방식은 장점이 있다. 위와 같이 하나의 요소에 여러가지 이벤트 핸들러를 등록할 수 있다는 점이다. 해당코드는 버튼 요소에 click, mouseover, mouseout 이벤트에 대한 3개의 이벤트 핸들러 함수를 등록한다.
* 이벤트핸들러 함수를 해제하는 방법
요소.removeEventListener('이벤트 이름', '이벤트핸들러 함수')
요소의 removeEventListener 라는 메서드를 사용하면 등록된 이벤트핸들러 함수가 해제된다. removeEventListener 의 첫번째 인자로 'click' 'change' 와 같은 이벤트 이름을 전달하고, 두번째 인자에는 해제할 이벤트핸들러 함수를 설정해주면 된다.
function popup(){
alert('팝업창')
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
위 코드는 버튼이 클릭될때마다 경고창이 뜬다.
function popup(){
alert('팝업창')
popupBtn.removeEventListener('click', popup)
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
위 코드는 맨 처음 버튼을 클릭했을때 경고창이 뜬 다음 곧바로 removeEventListener 메서드를 사용하여 popup 이라는 이벤트핸들러 함수를 해제한다. 이렇게 되면 그 다음부터는 버튼을 클릭하더라도 경고창이 뜨지 않는다.
* 이벤트 객체 (이벤트핸들러 함수의 매개변수)
function popup(e){
console.log(e)
console.log(e.target)
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
이벤트가 실행되면 이벤트핸들러 함수에는 이벤트 객체가 자동으로 함수의 인자로 전달된다. e 는 이벤트 객체이며 해당 객체에는 아래와 같은 다양한 프로퍼티가 존재한다.
이중에서 type 과 target 프로퍼티가 유용하다. type 은 발생한 이벤트의 종류를 의미한다. target 은 이벤트가 발생된 요소를 가리킨다. 현재는 아래와 같이 클릭한 버튼을 가리킨다.
function popup(event){
console.log(event)
console.log(event.target)
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
이벤트 객체의 이름은 이벤트핸들러 함수 안에서 어떤 이름을 사용해도 된다. 하지만 개발자들이 주로 사용하는 이름은 e, evt, event 등이다.
function popup(e){
e.target.style.all = 'unset'
e.target.style.position = 'absolute'
e.target.style.left = '500px'
e.target.style.width = '200px'
e.target.style.height = '70px'
e.target.style.textAlign = 'center'
e.target.style.background = 'skyblue'
e.target.style.borderRadius = '50%'
e.target.style.transition = 'all ease 2.5s'
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
위 예제코드는 이벤트 타겟(e.target) 을 사용하여 버튼이 클릭될때 해당 버튼의 위치, 모양, 색상을 변경한다.
function popup(e){
const target = e.target
const targetStyle = target.style
targetStyle.all = 'unset'
targetStyle.position = 'absolute'
targetStyle.left = '500px'
targetStyle.width = '200px'
targetStyle.height = '70px'
targetStyle.textAlign = 'center'
targetStyle.background = 'skyblue'
targetStyle.borderRadius = '50%'
targetStyle.transition = 'all ease 2.5s'
}
const popupBtn = document.getElementById('popup')
popupBtn.addEventListener('click', popup)
물론 변수가 반복되는 코드는 좋은 코드가 아니다. 좀 더 나은 방법은 위와 같이 반복되는 변수를 다른 변수로 치환하는 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<button class='popup'>팝업창 열기</button>
<script src='app.js'></script>
</body>
</html>
좀 더 나은 스타일 변경 방식은 요소의 classList 프로퍼티를 사용하는 것이다. HTML 문서에서 button 요소에 id 가 아닌 class 속성을 설정한다.
.circle{
all: unset;
position: absolute;
left: 500px;
width: 200px;
height: 70px;
text-align: center;
background: skyblue;
border-radius: 50%;
transition: all ease 2.5s;
}
css 파일에는 변경할 스타일을 정의한다.
function popup(e){
const target = e.target
target.classList.add('circle')
}
const popupBtn = document.querySelector('.popup')
popupBtn.addEventListener('click', popup)
getElementById 메서드가 아닌 querySelector 메서도로 버튼 요소를 검색한 다음 이벤트핸들러 함수를 등록한다. 이벤트 핸들러 함수 내부에서는 이벤트가 발생한 타겟에 classList 프로퍼티로 변경할 스타일을 추가해주면 된다.
* 기본적인 이벤트 동작 제한하기
웹페이지에서 사용자 로그인을 처리하는 예제코드를 살펴보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>로그인 화면</h1>
<form action="/home">
<input type="text" id='user-id'>
<input type="password" id='user-password'>
<input type="submit">
</form>
<p></p>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
body{
padding: 0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
style.css 파일을 위와 같이 작성하자!
const form = document.querySelector('form')
const id = document.getElementById('user-id')
const password = document.getElementById('user-password')
const p = document.querySelector('p')
function login(){
alert('login is processing ...')
}
form.addEventListener('submit', login)
app.js 파일을 위와 같이 작성하자! 위 코드는 사용자가 로그인 아이디와 비밀번호를 입력하고 제출 버튼을 클릭했을때 경고창을 띄워서 로그인이 진행중이라는 메세지를 출력해준다. 경고창을 끄면 form 의 action 속성에 지정한 url 경로로 페이지를 이동시킨다.
만약 사용자가 로그인 정보를 입력하지 않고 제출 버튼을 클릭했을때는 /home 페이지로 이동하지 않게 하려면 어떻게 하면 될까?
const form = document.querySelector('form')
const id = document.getElementById('user-id')
const password = document.getElementById('user-password')
const p = document.querySelector('p')
function login(e){
if(id.value === '' || password.value === ''){
e.preventDefault()
p.innerText = '아이디나 비밀번호가 입력되지 않았습니다 !'
}else{
alert('login is processing ...')
}
}
form.addEventListener('submit', login)
app.js 파일을 위와 같이 수정하자! 위 코드는 로그인시 사용자 정보의 유효성 검증을 하는 기본적인 로직이다. 사용자가 아이디나 비밀번호 중 어느 하나라도 입력하지 않고 제출 버튼을 클릭하면 페이지를 이동하지 않고 사용자에게 잘못된 부분에 대하여 안내 메세지를 보여준다.
form 내부의 submit 버튼은 기본적으로 페이지를 이동하는 동작을 하는데 이벤트 객체의 preventDefault 메서드를 사용하게 되면 기본적인 이벤트 동작을 제한한다. 그래서 다른 페이지로 이동하지 않도록 한다.
* setTimeout 메서드
setTimeout(함수, 실행될 특정시각)
setTimeout 메서드는 브라우저의 API 이다. 첫번째 인자로 실행할 함수를 설정한다. 두번째 인자로는 함수를 실행할 특정 시각이다. 즉, 설정한 함수는 특정시각 뒤에 실행이 된다. 단위는 ms 이다.
setTimeout 메서드를 사용해서 인스턴트 메세지를 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class='instant-msg'>Instant Message</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
.instant-msg{
width: 500px;
height: 100px;
background: peru;
color: tan;
border-radius: 15px;
position: absolute;
top: 10px;
right: 10px;
display: none;
text-align: center;
line-height: 100px;
font-size: 1.5rem;
font-weight: bold;
}
.show{
display: block;
}
style.css 파일을 위와 같이 작성하자!
const instantMsg = document.querySelector('.instant-msg')
function showMsg(){
instantMsg.classList.add('show')
}
function hideMsg(){
instantMsg.classList.remove('show')
}
setTimeout(showMsg, 1000)
setTimeout(hideMsg, 3000)
app.js 파일을 위와 같이 작성하자!
setTimeout(showMsg, 1000)
1초 뒤에 showMsg 함수를 실행한다.
function showMsg(){
instantMsg.classList.add('show')
}
showMsg 함수에서는 instantMsg 라는 div 요소에 show 라는 클래스를 추가함으로써 메세지를 화면에 보여준다.
setTimeout(hideMsg, 3000)
3초 뒤에 hideMsg 함수를 실행한다.
function hideMsg(){
instantMsg.classList.remove('show')
}
hideMsg 함수에서는 instantMsg 라는 div 요소에 show 라는 클래스를 제거함으로써 메세지를 화면에서 사라지게 한다.
이번에는 setTimeout 메서드를 이용하여 리스트에서 특정 아이템을 사라지게 해보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="box">1<span>x</span></div>
<div class="box">2<span>x</span></div>
<div class="box">3<span>x</span></div>
<div class="box">4<span>x</span></div>
<div class="box">5<span>x</span></div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 코드를 위와 같이 작성하자. 전체 리스트를 감싸고 있는 컨테이너와 각각의 리스트 아이템이 있다. 그리고 아이템 중 어느 하나를 삭제하기 위한 X 버튼이 있다.
body{
margin: 0; padding: 0;
box-sizing: border-box;
}
.container{
position: absolute;
width: 100%;
height: 100vh;
background-color: #0e1111;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
gap: 1.5rem;
}
.box{
border: 3px solid wheat;
border-radius: 30px 0 30px 0;
width: 30vw; height: 5rem; line-height: 5rem;
color: wheat; font-size: 2rem;
text-align: left;
padding-left: 1rem;
opacity: 1;
transition: .3s ease-out;
}
.box span{
width: 3rem;
float: right;
margin-right: 1rem;
cursor: pointer;
transition: .3s;
text-align: center;
}
.box span:hover{
transform: rotateZ(90deg);
}
.fadeOut{
opacity: 0;
}
.hide{
display: none;
}
style.css 파일을 위와 같이 작성하자! 리스트 아이템의 opacity 는 처음에 1이다가 fadeOut 클래스가 적용되면 0이 된다.
const container = document.querySelector('.container')
function removeItem(e){
console.log(e.target.className)
if(e.target.className === ''){
const box = e.target.parentElement
box.classList.add('fadeOut')
setTimeout(function(){
box.classList.add('hide')
}, 300)
}
}
container.addEventListener('click', removeItem)
app.js 파일을 위와 같이 작성하자!
container.addEventListener('click', removeItem)
이벤트 위임을 사용하여 리스트 아이템을 감싸고 있는 컨테이너에 클릭 이벤트를 연결한다.
if(e.target.className === ''){
const box = e.target.parentElement
box.classList.add('fadeOut')
setTimeout(function(){
box.classList.add('hide')
}, 300)
}
클릭한 요소의 클래스명이 존재하지 않으면 X 버튼을 클릭한 경우이다. 이때 X 버튼의 부모요소인 box 에 fadeOut 클래스를 추가하여 1초동안 리스트 아이템이 서서히 보이지 않도록 한다. 그런 다음 1초 후에 클릭한 box 에 hide 클래스를 추가하여 해당 아이템이 화면에서 완전히 사라지게 한다.
* setInterval 메서드
setInterval(함수, 시간간격)
setInterval 메서드는 브라우저의 API 이다. 첫번째 인자로 실행할 함수를 설정한다. 두번째 인자로는 함수를 실행할 시간간격이다. 단위는 ms 이다. 이렇게 하면 함수가 시간간격만큼 지속적으로 실행이 된다.
setInterval 메서드를 사용해서 디지털 시계를 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel='stylesheet' href='style.css'/>
</head>
<body>
<div id='clock'></div>
<script src="app.js"></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
body{
margin: 0;
padding: 0;
box-sizing: border-box;
}
#clock{
width: 100%;
height: 100vh;
background: url('https://t1.daumcdn.net/cfile/tistory/99E3B33E5B2852742A') no-repeat center;
background-size: cover;
color: white;
font-size: 5rem;
display: flex;
justify-content: center;
align-items: center;
}
style.css 파일을 위와 같이 작성하자!
const clock = document.getElementById('clock')
function changeFormat(t){
return t < 10 ? `0${t}` : t
}
function getTime(){
const time = new Date()
const hours = time.getHours()
const minutes = time.getMinutes()
const seconds = time.getSeconds()
clock.innerHTML = `${changeFormat(hours)}:${changeFormat(minutes)}:${changeFormat(seconds)}`
}
setInterval(getTime, 1000)
app.js 파일을 위와 같이 작성하자!
setInterval(getTime, 1000)
1초 간격으로 계속 getTime 함수를 실행하면서 현재 시간을 조회한다.
const time = new Date()
Date 내장함수로 현재 시간을 조회한다.
const hours = time.getHours()
const minutes = time.getMinutes()
const seconds = time.getSeconds()
조회한 현재 시간에서 시, 분, 초 값을 가져온다.
const clock = document.getElementById('clock')
clock.innerHTML = `${changeFormat(hours)}:${changeFormat(minutes)}:${changeFormat(seconds)}`
clock 이라는 div 요소를 가져와서 내부 문자열로 시, 분, 초를 포맷에 맞춰 디스플레이한다.
* 이벤트종류 (키보드 이벤트, 마우스 이벤트)
마우스 이벤트
마우스를 클릭하면 클릭한 위치로 원(circle)이 이동하게 해보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel='stylesheet' href='style.css'/>
</head>
<body>
<div id="circle"></div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 원(circle) 을 그리기 위하여 div 요소를 추가하였다.
#circle{
width: 100px;
height: 100px;
border-radius: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: peru;
}
원(circle) 모양을 만들기 위하여 border-radius 속성을 50%로 설정하였다. 화면 중앙에 원이 위치하게 하기 위하여 position 속성을 absolute 로 설정하고, left 와 top 속성을 모두 50% 로 설정하였다.
const circle = document.getElementById('circle')
function moveCircle(e){
console.log(e.clientX, e.clientY)
const mouseX = e.clientX
const mouseY = e.clientY
circle.style.left = mouseX + 'px'
circle.style.top = mouseY + 'px'
}
window.addEventListener('click', moveCircle)
window 객체에 click 이벤트를 등록한다. window 는 웹화면 전체에서 클릭 이벤트를 감지한다. e 는 이벤트 객체이며, 클릭한 마우스 포인트의 위치는 e.clientX (X 좌표) 와 e.clientY (Y 좌표) 로 가져올 수 있다. 마우스 포인트 위치를 읽은 다음에 원(circle)의 left, top 속성을 마우스 포인트 위치로 설정하면 된다.
이때, left 와 top 속성은 px 단위이므로 문자열 연결을 사용하여 'px' 을 붙여주면 된다.
그렇다면 원(circle) 이 아니라 이미지를 이동시키려면 어떻게 하면 될까?
#circle{
width: 100px;
height: 100px;
border-radius: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: peru;
background: url('https://pocketables.com/wp-content/uploads/2018/03/hero_chara_mario_update_pc1.png') no-repeat center;
background-size: cover;
}
div 요소에 background 이미지를 설정하면 된다. 사진은 url 함수에 온라인의 이미지 경로를 설정하면 된다.
마우스 포인트를 따라다니는 커스텀 커서를 만들어보자!
const circle = document.getElementById('circle')
function moveCircle(e){
console.log(e.clientX, e.clientY)
const mouseX = e.clientX
const mouseY = e.clientY
circle.style.left = mouseX + 'px'
circle.style.top = mouseY + 'px'
}
window.addEventListener('mousemove', moveCircle)
위와 같이 window 객체에 click 이벤트 대신 mousemove 이벤트를 등록해주면 된다. 이렇게 하면 마우스가 이동할때마다 이벤트가 발생하고, 해당 마우스 포인트 위치로 원(circle)이 계속 이동하게 된다. 스타일 코드에 cursor: none 을 추가하면 더이상 커서가 보이지 않고 이미지만 커서처럼 움직인다.
마우스를 올려놓으면 깜빡거리는 led 를 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel='stylesheet' href='style.css'/>
</head>
<body>
<div id="led-container">
<div class="led"></div>
<div class="led"></div>
<div class="led"></div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! led 를 화면에 보여주기 위한 div 요소들을 추가한다. led 를 감싸는 led-container 가 있고, 내부에 3개의 led 가 있다.
body{
margin: 0;
padding: 0;
background: black;
}
#led-container{
display: flex;
justify-content: center;
align-items: center;
grid-gap: 30px;
/* border: 1px solid red; */
width: 500px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.led{
width: 30px;
height: 30px;
background: orange;
border-radius: 50%;
cursor: pointer;
transition: all 0.5s ease-in-out;
}
.on{
box-shadow: 1px 1px 10px 10px orange;
}
style.css 코드를 위와 같이 작성하자! led 가 중앙에 정렬될 수 있도록 led-container 의 position 속성을 absolute 로 설정한다. 그리고 lef 와 top 속성을 50% 로 설정한다. led 모양을 만들기 위하여 border-radius 속성을 50%로 설정한다. led 의 깜빡임을 디스플레이하기 위하여 transition 속성을 설정한다.
마우스가 led 에 위치하게 될때 깜빡일 수 있도록 on 클래스를 정의한다. on 클래스가 추가되면 box-shadow 가 설정되면서 led 가 깜빡이게 된다.
const ledContainer = document.getElementById('led-container')
const leds = document.querySelectorAll('.led')
function lighton(e){
e.target.classList.add('on')
}
function lightoff(e){
e.target.classList.remove('on')
}
for(let led of leds){
led.addEventListener('mouseover', lighton)
led.addEventListener('mouseleave', lightoff)
}
app.js 파일을 위와 같이 작성하자! 각각의 led 에 addEventListner 메서드로 마우스가 호버된 경우와 마우스가 요소에서 벗어난 경우에 대하여 이벤트핸들러 함수를 등록한다. 그런 다음 이벤트 객체(e)의 타겟(target)을 이용하여 이벤트가 발생한 요소(현재는 각각의 led)의 클래스명에 on 을 추가하거나 제거한다.
const ledContainer = document.getElementById('led-container')
const leds = document.querySelectorAll('.led')
function lighton(e){
console.log(e.target)
console.log(e.target.className)
if(e.target.className === 'led'){
e.target.classList.add('on')
}
}
function lightoff(e){
if(e.target.className === 'led on'){
e.target.classList.remove('on')
}
}
ledContainer.addEventListener('mouseover', lighton)
for(let led of leds){
led.addEventListener('mouseleave', lightoff)
}
app.js 파일을 위와 같이 수정하자!
const ledContainer = document.getElementById('led-container')
const leds = document.querySelectorAll('.led')
마우스 이벤트를 등록하기 위하여 ledContainer 와 leds 요소를 HTML 문서에서 각각 검색한다.
ledContainer.addEventListener('mouseover', lighton)
ledContainer 에 mouseover 이벤트를 등록하고 lighton 이벤트핸들러 함수를 연결한다. ledContainer 영역 안에 마우스가 위치하고 있으면 lighton 이벤트 핸들러함수가 실행된다.
function lighton(e){
console.log(e.target)
console.log(e.target.className)
if(e.target.className === 'led'){
e.target.classList.add('on')
}
}
lighton 이벤트핸들러 함수 안에서는 조건문을 사용해서 현재 마우스 포인트의 위치가 led 내부이면 led 를 켠다. 즉, 현재 마우스 포인트가 위치해 있는 영역의 클래스명이 led 이면 해당 요소의 클래스명에 on 을 추가함으로써 led 가 켜진다.
led 각각에 이벤트를 각각 등록하지 않고 led 를 감싸는 컨테이너에 이벤트를 등록하는 것을 이벤트 위임이라고 한다. 이벤트 위임은 나중에 자세히 설명한다.
for(let led of leds){
led.addEventListener('mouseleave', lightoff)
}
반복문을 사용해서 각각의 led 에 같은 이벤트핸들러 함수를 등록한다. 각각의 led 에 mouseleave 이벤트를 등록해서 마우스 포인트가 해당 led 영역을 벗어나면 lightoff 이벤트핸들러 함수가 실행되면서 해당 led 가 꺼진다.
function lightoff(e){
if(e.target.className === 'led on'){
e.target.classList.remove('on')
}
}
마우스 포인트가 해당 led 를 벗어나면 실행되는 이벤트핸들러 함수이다. lightoff 함수가 실행되면 클래스명이 'led on' 인 경우에 해당 led 의 on 클래스를 제거함으로써 led 가 꺼지게 한다.
만약 웹화면이 처음 보여질때 led 가 자동으로 깜빡이게 하려면 어떻게 하면 될까?
let index = 0 // 특정 led 를 선택하기 위한 인덱스값
function lightoff(){
const led = document.querySelector('.on')
if(led) led.classList.remove('on')
}
function lighton(){
lightoff() // 기존에 켜져있는 led off
const leds = document.querySelectorAll('.led')
leds[index].classList.add('on') // 현재 인덱스의 led on
index++
if(index > leds.length - 1){ // 인덱스값이 leds 배열의 마지막 인덱스에 도달하면 초기값으로 돌아감
index = 0
}
}
function startEffect(){
console.log('load')
setInterval(lighton,1000)
}
window.addEventListener('load', startEffect) // 웹화면이 초기 렌더링될때 실행
app.js 파일을 위와 같이 수정하자!
let index = 0
특정 led 를 선택하기 위하여 index 변수를 선언한다.
window.addEventListener('load', startEffect)
웹화면이 처음 로딩될때 led 깜빡임 효과가 자동으로 시작되도록 load 이벤트를 등록한다. load 이벤트는 웹화면이 처음 시작될때 발생한다. 웹화면이 처음 로딩되면 startEffect 이벤트핸들러 함수가 실행된다.
function startEffect(){
console.log('load')
setInterval(lighton,1000)
}
window 객체의 브라우저 API 인 setInterval 메서드를 사용하여 1초마다 lighton 함수가 실행되도록 한다. setInterval 의 첫번째 인자는 실행할 함수이고, 두번째 인자는 함수가 실행되는 시간 간격이다. ms 이므로 1000ms 는 1초가 된다. 즉, 1초마다 led 가 깜빡이게 한다.
function lighton(){
lightoff() // 기존에 켜져있는 led off
const leds = document.querySelectorAll('.led')
leds[index].classList.add('on') // 현재 인덱스의 led on
index++
if(index > leds.length - 1){ // 인덱스값이 leds 배열의 마지막 인덱스에 도달하면 초기값으로 돌아감
index = 0
}
}
lighton 함수는 setInterval 로 설정된 타이머에 의하여 1초마다 실행된다.
lightoff() // 기존에 켜져있는 led off
기존에 켜져있는 led 를 먼저 끈다.
const leds = document.querySelectorAll('.led')
leds[index].classList.add('on') // 현재 인덱스의 led on
querySelectorAll 메서드와 index 값을 사용하여 특정 led 를 선택한다. 해당 led 에 on 클래스를 추가함으로써 led 를 켜지게 한다.
index++
if(index > leds.length - 1){ // 인덱스값이 leds 배열의 마지막 인덱스에 도달하면 초기값으로 돌아감
index = 0
}
다음 led 를 선택하기 위하여 index 값을 증가시킨다. led 는 3개이므로 index 값은 0 부터 2까지만 증가해야 한다. 그래서 조건문을 사용하여 index 값이 leds 배열의 마지막 인덱스(2)보다 커지면 다시 0 으로 초기화한다.
function lightoff(){
const led = document.querySelector('.on')
if(led) led.classList.remove('on')
}
querySelector 메서드로 현재 켜져있는 led 를 검색한다. 만약 켜져있는 led 가 있으면 해당 led 에 on 클래스를 제거함으로써 led 를 끈다.
클릭 이벤트 처리
버튼을 클릭했을때 사이드바 메뉴가 열리는 예제를 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel='stylesheet' href='style.css'/>
</head>
<body>
<button id='open-btn'>open sidebar</button>
<div class="sidebar">
<div class="menu">HOME</div>
<div class="menu">ABOUT</div>
<div class="menu">CONTACT</div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 사이드바 메뉴를 열기 위한 버튼과 사이드바를 구성하는 요소가 정의되어 있다.
.sidebar{
width: 30%;
height: 100vh;
position: absolute;
left: -30%;
top: 0px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: all 0.7s ease-in-out;
}
.show{
left: 0px;
}
.menu{
width: 100%;
flex: 1;
background-color: peru;
font-size: 1.5rem;
display: flex;
justify-content: center;
align-items: center;
color: lightgoldenrodyellow;
border-bottom: 1px solid lightgoldenrodyellow;
}
.menu:hover{
color: peru;
background-color: lightgoldenrodyellow;
}
style.css 파일을 위와 같이 작성하자! 사이드바 메뉴에 대한 스타일 코드가 정의되어 있다. 메뉴를 감싸고 있는 div 요소에는 플렉스박스를 적용해서 메뉴들이 잘 정렬될 수 있도록 하였다. 각 메뉴에 flex 를 1 로 설정해서 모두 동일한 높이를 가지도록 하였다. 각 메뉴에도 플렉스박스를 적용해서 내부의 컨텐츠가 가운데 정렬될 수 있도록 하였다.
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
function openSidebar(){
sidebar.classList.add('show')
}
openBtn.addEventListener('click', openSidebar)
app.js 파일을 위와 같이 작성하자!
사이드바가 열리는 원리는 간단하다. 우선 클래스명이 sidebar 인 div 요소의 position 을 absolute 로 설정한다. 그런 다음 width 값을 30% 로 설정하고 left 를 -30% 로 설정해서 width 값만큼 왼쪽으로 이동시켜주면 사이드바가 보이지 않게 된다. 사용자가 버튼을 클릭하면 클래스명이 sidebar 인 div 요소에 새로운 클래스인 show 가 추가되면서 left 가 0 으로 변경된다. 그렇게 되면 사이드바가 width 값만큼 오른쪽으로 이동하면서 보이게 된다.
그럼 버튼을 클릭하지 않고 버튼에 마우스 포인트가 놓여질때 사이드바 메뉴가 열리게 하려면 어떻게 하면 될까?
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
function openSidebar(){
sidebar.classList.add('show')
}
openBtn.addEventListener('mouseenter', openSidebar)
마우스 이벤트인 mouseenter 를 사용하면 된다. mouseenter 이벤트는 마우스 포인트가 해당 요소의 영역 안으로 들어갈때 발생한다. 즉, 마우스 포인트가 버튼 요소의 영역 안으로 들어가면 사이드바 메뉴가 나타나게 된다.
만약 메뉴를 클릭했을때 사이드바를 닫으려면 어떻게 하면 될까?
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
function openSidebar(){
sidebar.classList.add('show') // 사이드바 열기
}
function closeSidebar(e){
if(e.target.className === 'menu'){ // 이벤트 위임 (인터뷰 가면 자주 물어보는 질문)
sidebar.classList.remove('show') // 사이드바 닫기
}
}
openBtn.addEventListener('mouseover', openSidebar)
sidebar.addEventListener('click', closeSidebar)
위와 같이 app.js 파일을 수정하자! 위와 같이 하면 메뉴를 클릭했을때 사이드바가 닫히게 된다.
만약 사이드바 영역 이외의 영역에 마우스를 클릭할때 사이드바가 닫히게 하려면 어떻게 하면 될까?
https://developer.mozilla.org/en-US/docs/Web/API/Node/contains
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
function openSidebar(e){
e.stopPropagation() // 이벤트 버블링 방지 (해당라인이 없으면 부모요소의 클릭이벤트인 hideSidebar 도 동시에 실행됨)
sidebar.classList.add('show')
}
function hideSidebar(e){
console.log(sidebar.classList.contains('show'))
if(sidebar.classList.contains('show') && !sidebar.contains(e.target)){ // 사이드바가 열린 경우 && 사이드바 영역을 클릭하지 않은 경우
console.log(sidebar.contains(e.target))
sidebar.classList.remove('show')
}
}
openBtn.addEventListener('click', openSidebar)
document.addEventListener('click', hideSidebar)
요소의 classList 프로퍼티에는 contains 메서드가 존재한다. 즉 해당 클래스 이름이 포함되어 있는지 검사한다. 사이드바에 show 클래스가 존재하면 사이드바가 열려 있는 상태이다. 또한, 요소의 contains 메서드는 해당 요소가 인자로 주어진 요소를 자식요소로 포함하는지 검사한다. 만약 사용자가 사이드바 영역을 클릭하면 sidebar 요소는 자식요소인 menu (e.target) 요소를 포함한다. 그래서 결국은 사이드바가 열려있고 사용자가 사이드바 영역 바깥을 클릭한 경우에만 사이드바가 닫힌다.
만약 메뉴를 클릭했을때도 사이드바가 닫히게 하고 싶으면 아래와 같이 메뉴에 클릭 이벤트를 등록해주면 된다.
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
const menus = document.querySelectorAll('.menu')
function openSidebar(e){
e.stopPropagation() // 이벤트 버블링 방지
sidebar.classList.add('show')
}
function closeSidebar(e){
// 사이드바가 열려있는지 체크하는 용도
console.log(sidebar.classList.contains('show'))
// 사이드바 영역 내부를 클릭했는지 체크하는 용도
console.log(sidebar.contains(e.target))
if(sidebar.classList.contains('show') && !sidebar.contains(e.target)){
sidebar.classList.remove('show')
}
}
function hideSidebar(){
sidebar.classList.remove('show')
}
openBtn.addEventListener('click', openSidebar)
document.addEventListener('click', closeSidebar)
for(let menu of menus){
menu.addEventListener('click', hideSidebar)
}
주의할점은 메뉴는 브라우저 안에 포함된 영역이기 때문에 메뉴의 클릭 이벤트핸들러 함수로 closeSidebar 를 설정하면 document 와 menu 모두 핸들러 함수가 실행된다. 이렇게 되면 document 에 대한 함수는 원하는대로 동작하지만 menu 에 대한 함수는 제대로 동작하지 않게 된다. 그러므로 메뉴에 대한 이벤트핸들러 함수는 closeSidebar 와 다른 이름을 사용해서 따로 구현해주어야 한다.
const openBtn = document.getElementById('open-btn')
const sidebar = document.querySelector('.sidebar')
const menus = document.querySelectorAll('.menu')
function openSidebar(e){
e.stopPropagation() // 이벤트 버블링 방지 (해당라인이 없으면 부모요소의 클릭이벤트인 hideSidebar 도 동시에 실행됨)
sidebar.classList.add('show')
}
function closeSidebar(e){
console.log('둘 다 실행됨')
console.log(sidebar.classList.contains('show'))
if(sidebar.classList.contains('show') && !sidebar.contains(e.target)){ // 사이드바가 열린 경우 && 사이드바 영역을 클릭하지 않은 경우
console.log(sidebar.contains(e.target))
sidebar.classList.remove('show')
}
if(sidebar.classList.contains('show') && sidebar.contains(e.target)){ // 사이드바가 열린 경우 && 사이드바 영역을 클릭하지 않은 경우
// console.log(sidebar.contains(e.target))
sidebar.classList.remove('show')
}
}
function hideSidebar(){
sidebar.classList.remove('show')
}
openBtn.addEventListener('click', openSidebar)
document.addEventListener('click', closeSidebar)
for(let menu of menus){
menu.addEventListener('click', closeSidebar)
}
이렇게 하더라도 동작은 되지만, 따로 함수로 빼주는 것이 좋다. 그렇지 않으면 메뉴를 클릭할때 이벤트버블링에 의하여 document 객체에 연결된 클릭 이벤트도 동시에 실행이 된다.
커로셀 만들기
마우스 이벤트로 간단한 커로셀(carousel)을 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Carosel</h1>
<div id='photo-container'>
<div id="photos">
<div class='photo'></div>
<div class="photo"></div>
<div class="photo"></div>
<div class="photo"></div>
<div class="photo"></div>
</div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
body{
margin: 0;
padding: 0;
text-align: center;
}
#photo-container{
width: 500px;
height: 250px;
margin: 100px auto;
overflow: hidden;
}
#photos{
width: 2500px;
height: 100%;
cursor: pointer;
display: flex;
transition: all 1s;
margin-left: -500px;
}
.photo{
flex: 1;
height: 100%;
overflow: hidden;
background-size: cover;
}
.photo:nth-child(1) {
background: url("https://media.istockphoto.com/photos/forest-wooden-table-background-summer-sunny-meadow-with-green-grass-picture-id1353553203?b=1&k=20&m=1353553203&s=170667a&w=0&h=QTyTGI9tWQluIlkmwW0s7Q4z7R_IT8egpzzHjW3cSas=") no-repeat center;
}
.photo:nth-child(2) {
background: url("https://digitalassets-retail.cdn-apple.com/retail-image-server/a90/c85/1bd/c2d/1af/8ba/a2a/349/93e/fe2/11b8d32a-7f9f-372c-bca3-0a05d871e8c7_iMac_Wallpaper_4480x2520.jpg") no-repeat center;
}
.photo:nth-child(3) {
background: url("http://file.instiz.net/data/file/20121125/5/6/0/5609249762358c2bcb65a46580549c99") no-repeat center;
}
.photo:nth-child(4) {
background: url("https://s1.pearlcdn.com/KR/Upload/Community/434174d044320230412231123488.jpg") no-repeat center;
}
.photo:nth-child(5){
background: url('https://www.institutostrom.org/wp-content/uploads/2018/04/NZ.jpg') no-repeat center;
}
style.css 파일을 위와 같이 작성하자! 사진의 너비 (photo-container)는 500px 로 설정한다. 사진들을 감싸는 컨테이너인 photos 라는 요소는 너비를 2500px 로 설정한다. 각각의 사진에는 flex: 1을 설정해서 사진을 담고 있는 컨테이너 너비 2500px 을 5등분해서 균등하게 분배한다. 즉, 각각의 사진의 너비는 500px 이 된다. 이렇게 한 다음 photos 의 왼쪽 마진을 500px 씩 감소시키면 사진이 왼쪽으로 이동한다.
const photos = document.getElementById('photos')
const widthOfPhoto = 500 // 사진너비
let marginLeft = widthOfPhoto // 사진이동 간격
let timerID = null // 타이머 ID
function changePosition(){
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
photos.style.marginLeft = marginLeft*-1 + 'px' // -1 : 왼쪽으로 이동
marginLeft = marginLeft >= (widthOfPhoto * (photosLength - 1)) ? 0 : marginLeft + widthOfPhoto // 사진 너비만큼 이동
}
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
function stopCarousel(){
clearInterval(timerID)
}
photos.addEventListener('mouseenter', startCarousel)
photos.addEventListener('mouseleave', stopCarousel)
app.js 파일을 위와 같이 작성하자!
const photos = document.getElementById('photos')
photos 요소에 마우스 이벤트를 등록하기 위하여 해당 요소를 검색한다.
photos.addEventListener('mouseenter', startCarousel)
마우스가 photos 요소에 hover 될때 커로셀이 동작하도록 한다.
let timerID = null
setInterval 의 타이머 ID 값을 저장하기 위하여 변수를 선언한다.
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
마우스가 photos 요소에 hover 될때 실행되는 이벤트핸들러 함수이다. 마우스가 hover 될때마다 setInterval 메서드를 이용하여 changePosition 함수가 1초마다 실행되도록 한다. setInterval 은 타이머 ID 를 반환하며 추후에 타이머 ID 를 사용하여 동작중인 커로셀을 정지시킬 것이다.
const widthOfPhoto = 500 // 사진너비
커로셀에서 보여줄 사진 한장의 너비이다.
let marginLeft = widthOfPhoto // 사진이동 간격
1초마다 이동할 사진의 간격이다. 1초마다 500px 씩 이동하며, 처음에는 500px 만큼 이동하므로 초기값은 widthOfPhoto 이다.
function changePosition(){
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
photos.style.marginLeft = marginLeft*-1 + 'px' // -1 : 왼쪽으로 이동
marginLeft = marginLeft >= (widthOfPhoto * (photosLength - 1)) ? 0 : marginLeft + widthOfPhoto // 사진 너비만큼 이동
}
setInterval 메서드에 의하여 1초마다 실행되는 함수이다. 전체 사진을 감싸고 있는 photos 요소는 1초마다 왼쪽으로 500px 씩 이동하면 된다. marginLeft 는 초기값이 500 이며 -1 이므로 처음에는 500px 만큼 왼쪽으로 이동한다. 1초마다 marginLeft (이동할 간격)은 widthOfPhoto(사진너비)만큼 증가한다. marginLeft 는 500만큼 계속 증가하다가 마지막 사진에 도달하면(marginLeft 가 2000이 되는 순간) 다시 0으로 초기화되어 첫번째 사진을 보여준다.
function stopCarousel(){
clearInterval(timerID)
}
마우스 포인트가 사진에서 벗어나면 clearInterval 메서드를 이용하여 타이머를 정지시킨다. 그러면 사진은 더이상 이동하지 않는다.
마지막 슬라이드에서 첫번째 슬라이드로 넘어갈때 많은 거리를 이동하게 된다. 그렇다면 마지막 슬라이드에서 이동하는 슬라이드 효과를 가리려면 어떻게 하면 될까?
const photos = document.getElementById('photos')
const widthOfPhoto = 500 // 사진너비
let marginLeft = widthOfPhoto // 사진이동 간격
let timerID = null // 타이머 ID
function changePosition(){
if(marginLeft === 0){ // 마지막 사진일때 첫번째 사진으로 슬라이드 되기전 가렸다가 다시 보여줌
photos.style.display = 'none'
setTimeout(function(){
photos.style.display = 'flex'
}, 100)
}
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
photos.style.marginLeft = marginLeft*-1 + 'px' // -1 : 왼쪽으로 이동
marginLeft = marginLeft >= (widthOfPhoto * (photosLength - 1)) ? 0 : marginLeft + widthOfPhoto // 사진 너비만큼 이동
}
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
function stopCarousel(){
clearInterval(timerID)
}
photos.addEventListener('mouseenter', startCarousel)
photos.addEventListener('mouseleave', stopCarousel)
마지막 사진인 경우 사진을 감싸고 있는 컨테이너를 잠깐 화면에서 가렸다가 setTimeout 을 이용하여 사용자가 눈치채지 못할만큼 짧은 순간(0.1초)후에 다시 보여주면 된다.
const photoContainer = document.getElementById('photo-container')
const photos = document.getElementById('photos')
const widthOfPhoto = 500 // 사진너비
let marginLeft = widthOfPhoto // 사진이동 간격
let timerID = null // 타이머 ID
function changePosition(){
if(marginLeft === 0){ // 마지막 사진일때 첫번째 사진으로 슬라이드 되기전 가렸다가 다시 보여줌
photos.style.display = 'none'
setTimeout(function(){
photos.style.display = 'flex'
}, 100)
}
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
photos.style.marginLeft = marginLeft*-1 + 'px' // -1 : 왼쪽으로 이동
marginLeft = marginLeft >= (widthOfPhoto * (photosLength - 1)) ? 0 : marginLeft + widthOfPhoto // 사진 너비만큼 이동
}
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
function stopCarousel(){
clearInterval(timerID)
}
photoContainer.addEventListener('mouseenter', startCarousel)
photoContainer.addEventListener('mouseleave', stopCarousel)
photos 요소에 마우스 호버 이벤트를 주니까 첫번째 사진에 도달했을때 더이상 슬라이드가 넘어가지 않아서 사진이 보여지는 상위 컨테이너에 마우스 이벤트를 설정하였다.
커로셀에 셀렉터(selector)를 추가해보자! 현재 보여지는 사진의 순서에 따라 인디케이터가 표시된다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Carosel</h1>
<div id='photo-container'>
<div id="photos">
<div class='photo'></div>
<div class="photo"></div>
<div class="photo"></div>
<div class="photo"></div>
<div class="photo"></div>
</div>
</div>
<div id='selection'>
<div class="options active"></div>
<div class="options"></div>
<div class="options"></div>
<div class="options"></div>
<div class="options"></div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 수정하자! 커로셀을 선택할 수 있는 selector 가 추가되었다.
body{
margin: 0;
padding: 0;
text-align: center;
}
#photo-container{
width: 500px;
height: 250px;
margin: 100px auto;
overflow: hidden;
}
#photos{
width: 2500px;
height: 100%;
cursor: pointer;
display: flex;
transition: all 1s;
/* margin-left: -1000px; */
}
.photo{
flex: 1;
height: 100%;
overflow: hidden;
background-size: cover;
}
.photo:nth-child(1) {
background: url("https://media.istockphoto.com/photos/forest-wooden-table-background-summer-sunny-meadow-with-green-grass-picture-id1353553203?b=1&k=20&m=1353553203&s=170667a&w=0&h=QTyTGI9tWQluIlkmwW0s7Q4z7R_IT8egpzzHjW3cSas=") no-repeat center;
}
.photo:nth-child(2) {
background: url("https://prod-virtuoso.dotcmscloud.com/dA/188da7ea-f44f-4b9c-92f9-6a65064021c1/previewImage/PowerfulReasons_hero.jpg") no-repeat center;
}
.photo:nth-child(3) {
background: url("http://file.instiz.net/data/file/20121125/5/6/0/5609249762358c2bcb65a46580549c99") no-repeat center;
}
.photo:nth-child(4) {
background: url("https://s1.pearlcdn.com/KR/Upload/Community/434174d044320230412231123488.jpg") no-repeat center;
}
.photo:nth-child(5){
background: url('https://www.institutostrom.org/wp-content/uploads/2018/04/NZ.jpg') no-repeat center;
}
/* 커로셀 셀렉터 */
#selection{
width: 300px;
height: 50px;
margin: 0px auto;
display: flex;
justify-content: center;
align-items: center;
}
.options{
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 10px;
background-color: lightgreen;
box-shadow: 1px 1px 5px 2px lightgreen;
user-select: none; /* 커서 제거 */
transition: all 0.7s;
}
.options:hover{
background-color: greenyellow;
cursor: pointer;
}
.options.active{
width: 50px;
}
style.css 파일을 위와 같이 수정하자!
const photos = document.getElementById('photos')
const widthOfPhoto = 500 // 사진너비
let marginLeft = widthOfPhoto // 사진이동 간격
let timerID = null // 타이머 ID
function changeIndicator(index){
const selection = document.getElementById('selection')
const activeOption = selection.querySelector('.active')
if(activeOption) activeOption.classList.remove('active') // 이전 active 한 인디케이터 제거
selection.querySelectorAll('.options')[index].classList.add('active') // 현재 인디케이터에 active 추가
}
function changePosition(){
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
photos.style.marginLeft = marginLeft*-1 + 'px' // -1 : 왼쪽으로 이동
const index = parseInt(marginLeft / widthOfPhoto) // 현재 사진의 인덱스값
changeIndicator(index) // 인디케이터 변경
marginLeft = marginLeft >= (widthOfPhoto * (photosLength - 1)) ? 0 : marginLeft + widthOfPhoto // 사진 너비만큼 이동
}
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
function stopCarousel(){
clearInterval(timerID)
}
photos.addEventListener('mouseenter', startCarousel)
photos.addEventListener('mouseleave', stopCarousel)
app.js 파일을 위와 같이 수정하자!
const index = parseInt(marginLeft / widthOfPhoto) // 현재 사진의 인덱스값
changeIndicator(index) // 인디케이터 변경
먼저 현재 사진의 순서(인덱스)를 알아내야 한다. marginLeft 는 0, 500, 1000, 1500, 2000 중의 하나를 가지면 widthOfPhoto 는 500 이므로 나눠주면 0, 1 , 2, 3, 4 의 인덱스 값을 알아낼 수 있다. 그런 다음 changeIndicator 함수를 실행한다.
function changeIndicator(index){
const selection = document.getElementById('selection')
const activeOption = selection.querySelector('.active') // 기존에 active 가 적용된 인디케이터
if(activeOption) activeOption.classList.remove('active') // 이전 active 한 인디케이터 제거
selection.querySelectorAll('.options')[index].classList.add('active') // 현재 인디케이터에 active 추가
}
먼저 기존에 active 가 적용된 인디케이터(options 요소)를 찾아야 한다. 그 다음 해당 인디케이터에 적용된 active 스타일을 제거한다. 마지막으로 현재 보여지는 사진의 인덱스(index) 값을 이용하여 해당 인디케이터(options 요소)에 active 스타일을 추가해주면 된다. 결과는 아래와 같다.
marginLeft 가 아니라 index 값을 사용하여 코드를 다시 작성하면 아래와 같다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Carosel</h1>
<div id='photo-container'>
<div id="photos">
<div class='photo' id="0"></div>
<div class="photo" id="1"></div>
<div class="photo" id="2"></div>
<div class="photo" id="3"></div>
<div class="photo" id="4"></div>
</div>
</div>
<div id='selection'>
<div class="options active" data-id="0"></div>
<div class="options" data-id="1"></div>
<div class="options" data-id="2"></div>
<div class="options" data-id="3"></div>
<div class="options" data-id="4"></div>
</div>
<script src='app.js'></script>
</body>
</html>
const photos = document.getElementById('photos')
const photosLength = photos.querySelectorAll('.photo').length // 사진의 총 갯수
const selection = document.querySelector('#selection') // 인디케이터 컨테이너
const widthOfPhoto = 500 // 사진너비
let timerID = null // 타이머 ID
let index = 0 // 사진 인덱스
function changeIndicator(index){
const prevIndicator = selection.querySelector('.active') // 이전 인디케이터 비활성화
prevIndicator.classList.remove('active')
const activeIndicator = selection.querySelectorAll('.options')[index] // 현재 인디케이터 활성화
activeIndicator.classList.add('active')
}
function changePosition(){
index++
if(index > photosLength - 1){ // 인덱스 초기화
index = 0
}
photos.style.marginLeft = -1 * index * widthOfPhoto + 'px' // 왼쪽으로 이동
changeIndicator(index) // 인디케이터 변경
}
function startCarousel(){
timerID = setInterval(changePosition, 1000)
}
function stopCarousel(){
clearInterval(timerID)
}
photos.addEventListener('mouseenter', startCarousel)
photos.addEventListener('mouseleave', stopCarousel)
윈도우 Load 이벤트
윈도우 load 이벤트를 사용하여 간단한 로딩화면을 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Loading ...</h1>
<div id="box-container">
<div class="box"></div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
body{
margin: 0;
padding: 0;
text-align: center;
}
#box-container{
position: relative;
width: 200px;
height: 200px;
margin: 100px auto;
background-color: lightsalmon;
}
.box{
position: absolute;
left: 0px;
top: 0px;
width: 100px;
height: 100px;
background-color: lightpink;
transition: all 1s;
}
style.css 파일을 위와 같이 작성하자! box 가 box-container 안에서 움직일 수 있도록 부모인 box-container 의 position 속성은 relative 로 설정하고 자식인 box 의 position 속성은 abosolute 로 설정하였다.
const box = document.querySelector('.box')
const moves = [
{pos: 'left', length: 100},
{pos: 'top', length: 100},
{pos: 'left', length: 0},
{pos: 'top', length: 0}
]
let select = 0
function moveBox(){
const pos = moves[select].pos
const length = moves[select].length
box.style[pos] = length + 'px'
select++
if(select > moves.length - 1){
select = 0
}
}
function startMove(){
setInterval(moveBox, 1000)
}
window.addEventListener('load', startMove)
app.js 파일을 위와 같이 작성하자!
const box = document.querySelector('.box')
box 요소의 위치를 변경하기 위하여 검색한다.
const moves = [
{pos: 'left', length: 100},
{pos: 'top', length: 100},
{pos: 'left', length: 0},
{pos: 'top', length: 0}
]
객체들의 배열을 사용하여 각각의 움직임을 정의한다. 첫번째 케이스는 오른쪽으로 100px 만큼 이동하는 것이다. 두번째 케이스는 아래쪽으로 100px 만큼 이동하는 것이다. 세번째 케이스는 왼쪽으로 100px 만큼 이동하는 것이다. 네번째 케이스는 위쪽으로 100px 만큼 이동하는 것이다.
let select = 0
특정 움직임을 선택하기 위하여 select 변수를 선언한다.
window.addEventListener('load', startMove)
윈도우가 처음 로딩되면 startMove 라는 이벤트핸들러 함수를 실행시킨다.
function startMove(){
setInterval(moveBox, 1000)
}
startMove 함수에서는 1초마다 moveBox 함수가 실행되도록 한다.
function moveBox(){
const pos = moves[select].pos
const length = moves[select].length
box.style[pos] = length + 'px'
select++
if(select > moves.length - 1){
select = 0
}
}
select 변수를 사용하여 특정 움직임을 선택한다. 해당 움직임의 pos, length 값을 이용하여 box 요소의 위치를 이동시킨다. 다음 움직임을 선택하기 위하여 select 변수를 업데이트한다. select 값이 moves 배열의 길이를 넘어가면 초기화한다.
마우스 클릭 이벤트를 사용하여 아코디언 메뉴를 만들어보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="menu-container">
<div class="menu">
<div class='title'>사과</div>
<div class='info'>
<h4>단가 : 700원</h4>
<h4>색깔 : 빨간색</h4>
</div>
</div>
<div class="menu">
<div class='title'>바나나</div>
<div class='info'>
<h4>단가 : 200원</h4>
<h4>색깔 : 노란색</h4>
</div>
</div>
<div class="menu">
<div class='title'>오렌지</div>
<div class='info'>
<h4>단가 : 1000원</h4>
<h4>색깔 : 주황색</h4>
</div>
</div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 과일에 대한 정보를 보여주는 아코디언을 만들 것이다.
body{
margin: 0;
padding: 0;
}
#menu-container{
width: 500px;
margin: 100px auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
/* border: 1px solid red;/ */
}
.menu{
width: 100%;
flex: 1;
margin-bottom: 10px;
cursor: pointer;
user-select: none;
}
.title{
background: lightgray;
padding: 10px;
}
.info{
padding: 10px;
background: lightgoldenrodyellow;
color: goldenrod;
font-weight: bold;
display: none;
}
.open{
display: block;
animation: fadeIn 1s ease-in-out forwards;
}
@keyframes fadeIn{
from{
opacity: 0;
}
to{
opacity: 1;
}
}
style.css 파일을 위와 같이 작성하자! info 요소는 일단 보이지 않게 한 다음 open 이라는 클래스며이 추가되면 보이게 할 것이다.
const menuContainer = document.getElementById('menu-container')
function openMenu(e){
const target = e.target
if(target.className === 'title'){
console.dir(target)
target.nextElementSibling.classList.toggle('open')
}
}
menuContainer.addEventListener('click', openMenu)
app.js 파일을 위와 같이 작성하자!
const menuContainer = document.getElementById('menu-container')
menu-container 요소를 검색한다.
menuContainer.addEventListener('click', openMenu)
menu-container 요소에 click 이벤트를 등록하고 openMenu 라는 이벤트핸들러 함수를 연결한다.
const target = e.target
사용자가 클릭한 메뉴가 무엇인지 알아내기 위하여 이벤트 객체의 target 프로퍼티를 조회한다.
if(target.className === 'title'){
console.dir(target)
target.nextElementSibling.classList.toggle('open')
}
사용자가 클릭한 요소의 클래스명이 title 이면 해당 요소의 다음 형제요소에 open 클래스명을 추가 또는 제거한다. 다음 형제요소는 info 요소이므로 해당 메뉴에 대한 정보가 보이거나 사라지게 한다.
버튼 클릭 이벤트로 간단한 페이지네이션을 구현해보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id='contents'></div>
<div id="page-btns"></div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 페이지네이션을 위한 버튼들이 위치할 page-btns 요소를 만들고, 특정 버튼을 클릭할때 페이지 내용을 보여줄 contents 라는 요소를 추가한다.
body{
margin: 0;
padding: 0;
text-align: center;
}
#contents{
width: 100%;
height: 100vh;
background-color: thistle;
color: white;
font-size: 5rem;
font-weight: bold;
line-height: 100vh;
}
#page-btns{
position: fixed;
bottom: 50px;
left: 50%;
transform: translate(-50%);
}
.page-btn{
width: 50px;
height: 50px;
background-color: purple;
border-radius: 10px;
margin-right: 5px;
cursor: pointer;
color: white;
font-size: 1rem;
font-weight: bold;
border: none;
outline: none;
}
style.css 파일을 위와 같이 작성하자! 페이지네이션 버튼들은 웹페이지 하단에 위치해야 하므로 position 을 fixed 로 설정하고 bottom 속성을 50px 로 설정한다.
const pageContents = [
'page 1', 'page 2', 'page 3', 'page 4', 'page 5',
'page 6', 'page 7', 'page 8', 'page 9', 'page 10',
]
const contents = document.getElementById('contents')
const pageBtns = document.getElementById('page-btns')
for(let i=0;i<pageContents.length;i++){
const btn = document.createElement('button')
btn.className = 'page-btn'
btn.innerText = i + 1
pageBtns.appendChild(btn)
}
contents.innerHTML = pageContents[0]
function changePage(e){
const target = e.target
if(target.className === 'page-btn'){
console.log(target)
const indexSelected = parseInt(target.innerText) - 1
console.log(indexSelected)
contents.innerHTML = pageContents[indexSelected]
}
}
pageBtns.addEventListener('click', changePage)
app.js 파일을 위와 같이 작성하자!
const pageContents = [
'page 1', 'page 2', 'page 3', 'page 4', 'page 5',
'page 6', 'page 7', 'page 8', 'page 9', 'page 10',
]
페이지네이션 버튼을 클릭하면 보여줄 페이지 내용을 배열로 선언한다.
for(let i=0;i<pageContents.length;i++){
const btn = document.createElement('button')
btn.className = 'page-btn'
btn.innerText = i + 1
pageBtns.appendChild(btn)
}
pageContents 배열을 사용하여 페이지수만큼 버튼을 생성한다. 그런 다음 생성한 버튼들을 pageBtns 요소에 추가하여 화면에 페이지네이션 버튼을 보여준다.
contents.innerHTML = pageContents[0]
맨 처음 화면이 로딩되었을때의 페이지 내용은 pageContents[0] 로 보여준다.
const pageBtns = document.getElementById('page-btns')
페이지네이션 버튼에 직접 click 이벤트를 적용하지 않고, 버튼들을 감싸고 있는 부모요소에 click 이벤트를 등록하기 위하여 pageBtns 요소를 검색한다.
pageBtns.addEventListener('click', changePage)
페이지네이션 버튼들을 감싸고 있는 pageBtns 라는 부모요소에 click 이벤트를 등록하고, changePage 이벤트핸들러 함수를 연결한다. 이를 이벤트 위임이라고 한다.
function changePage(e){
const target = e.target
if(target.className === 'page-btn'){
console.log(target)
const indexSelected = parseInt(target.innerText) - 1
console.log(indexSelected)
contents.innerHTML = pageContents[indexSelected]
}
}
페이지네이션 버튼이 클릭되면 실행할 이벤트핸들러 함수이다. 이벤트 위임을 사용하기 때문에 부모요소인 pageBtns 도 클릭될수 있고, 자식요소인 버튼들도 클릭될 수 있다. 그래서 클릭한 타겟요소(e.target)의 클래스명이 page-btn 일때만 페이지를 업데이트할 수 있도록 한다.
페이지네이션 버튼을 클릭하면 target.innerText 값을 이용하여 페이지 번호(indexSelected)를 알아낸다. 그런 후에 pageContents 배열에서 페이지번호에 해당하는 페이지 내용으로 contents 요소의 컨텐츠를 변경한다.
페이지 내용을 문자열이 아닌 리스트로 변경해보자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id='contents'></div>
<div id="page-btns"></div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자!
body{
margin: 0;
padding: 0;
text-align: center;
}
#contents{
width: 100%;
min-height: 100vh;
background-color: thistle;
color: white;
font-weight: bold;
/* padding: 15px; */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.list-item{
width: 500px;
height: 100px;
background-color: white;
color: purple;
margin: 10px;
}
#page-btns{
position: fixed;
bottom: 50px;
left: 50%;
transform: translate(-50%);
}
.page-btn{
width: 50px;
height: 50px;
background-color: purple;
border-radius: 10px;
margin-right: 5px;
cursor: pointer;
color: white;
font-size: 1rem;
font-weight: bold;
border: none;
outline: none;
}
style.css 파일을 위와 같이 작성하자!
const pageContents = [] // 데이터를 담고 있는 배열
const numOfData = 100 // 전체 리스트 수
const limit = 4 // 페이지당 보여줄 리스트 수
const numOfBtns = Math.ceil(numOfData / limit)
let offset = 0
console.log(numOfBtns)
const contents = document.getElementById('contents')
const pageBtns = document.getElementById('page-btns')
// 리스트 배열 만들기
for(let i=0;i<numOfData;i++){
pageContents.push({ name: 'sunrise', age: 20, id: i })
}
console.log(pageContents)
// 화면에 페이지네이션 버튼 보여주기
for(let i=0;i<numOfBtns;i++){
const btn = document.createElement('button')
btn.className = 'page-btn'
btn.innerText = i + 1
pageBtns.appendChild(btn)
}
// 첫페이지 로딩
for(let i=offset; i<offset+limit; i++){
const listItem = pageContents[i]
contents.innerHTML += `
<div id=${listItem.id} class='list-item'>
<h3>${listItem.name} (${listItem.id})</h3>
<h3>${listItem.age}</h3>
</div>
`
}
function changePage(e){
const target = e.target
if(target.className === 'page-btn'){
console.log(target)
const indexSelected = parseInt(target.innerText) - 1
console.log(indexSelected)
offset = limit * indexSelected
console.log(offset) // 콘텐츠 시작점(오프셋) = (페이지번호 - 1) x 페이지당 보여줄 갯수
const listSelected = []
// offset ~ offset + limit - 1 까지의 리스트만 추출하기
for(let i=offset; i<offset + limit;i++){
const item = pageContents[i]
listSelected.push(item)
}
console.log(listSelected)
// 화면 초기화
contents.innerHTML = ''
// 화면에 특정 페이지 리스트만 보여주기
for(let listItem of listSelected){
contents.innerHTML += `
<div id=${listItem.id} class='list-item'>
<h3>${listItem.name} (${listItem.id})</h3>
<h3>${listItem.age}</h3>
</div>
`
}
}
}
pageBtns.addEventListener('click', changePage)
app.js 파일을 위와 같이 작성해보자!
* 이벤트 캡쳐링과 버블링
이벤트 캡쳐링은 body 요소부터 시작해서 이벤트가 발생한 요소까지 내려가면서 이벤트핸들러 함수가 실행된다. addEventListner 메서드의 세번째 인자로 true 값을 설정하면 캡쳐링으로 동작한다.
element.addEventListener('이벤트 이름', 이벤트핸들러 함수, true)
이벤트 버블링은 캡쳐링과는 반대로 이벤트가 발생한 요소부터 시작해서 body 요소까지 올라가면서 이벤트핸들러 함수가 실행된다. addEventListner 메서드의 세번째 인자로 false 값을 설정하면 버블링으로 동작한다. 디폴트값으로 false 가 설정되어 있으므로 굳이 설정할 필요는 없다.
element.addEventListener('이벤트 이름', 이벤트핸들러 함수, false)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="parent">
<button id='child'>클릭</button>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 클래스명이 parent 인 부모요소를 만들고, 내부에 id 값이 child 인 버튼 요소를 추가한다.
.parent{
border: 10px solid orange;
padding: 50px;
text-align: center;
}
.hide{
display: none;
}
#child{
display: inline-block;
all: unset;
width: 300px;
height: 100px;
line-height: 100px;
font-size: 1.5rem;
border-radius: 15px;
font-weight: bold;
background: peru;
color: white;
text-align: center;
cursor: pointer;
}
style.css 파일을 위와 같이 작성하자! 부모 요소를 숨기기 위하여 클래스명이 hide 인 스타일을 정의하였다.
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function clickbody(){
console.log('body clicked !')
}
function hideParent(){
console.log('parent clicked !')
// parent.classList.add('hide')
}
function popup(){
console.log('child clicked !')
}
parent.addEventListener('click', hideParent, true)
child.addEventListener('click', popup, true)
document.body.addEventListener('click', clickbody, true)
app.js 파일을 위와 같이 작성하자!
body 요소, parent 요소, child 요소에 각각 click 이벤트를 등록하였다. 자식요소인 button 을 클릭하면 아래와 같이 body 요소부터 시작해서 child 요소까지 내려가면서 이벤트핸들러 함수가 순차적으로 실행된다. 이는 addEventListener 메서드의 세번째 인자로 true 값을 설정함으로써 이벤트 캡쳐링이 적용되기 때문이다.
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function clickbody(){
console.log('body clicked !')
}
function hideParent(){
console.log('parent clicked !')
// parent.classList.add('hide')
}
function popup(){
console.log('child clicked !')
}
parent.addEventListener('click', hideParent)
child.addEventListener('click', popup)
document.body.addEventListener('click', clickbody)
body 요소, parent 요소, child 요소에 각각 click 이벤트를 등록하였다. 자식요소인 button 을 클릭하면 아래와 같이 child 요소부터 시작해서 body 요소까지 올라가면서 이벤트핸들러 함수가 순차적으로 실행된다. 이는 addEventListener 메서드의 세번째 인자로 false 값을 설정함으로써 (또는 설정하지 않음으로써) 이벤트 버블링이 적용되기 때문이다.
그럼 아래와 같이 click 이벤트는 동일하게 적용하지만 true, false 를 다르게 주면 어떻게 될까?
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function clickbody(){
console.log('body clicked !')
}
function hideParent(){
console.log('parent clicked !')
// parent.classList.add('hide')
}
function popup(){
console.log('child clicked !')
}
parent.addEventListener('click', hideParent, true)
child.addEventListener('click', popup, false)
document.body.addEventListener('click', clickbody, false)
이벤트 캡쳐링과 버블링이 동시에 일어난다. 우선 버튼을 클릭하면 body 에서부터 클릭한 버튼을 찾기 위하여 캡쳐링이 일어난다. 그 과정에서 parent 의 클릭 이벤트가 먼저 실행된다. 그런 다음 클릭한 버튼을 찾으면 버튼의 클릭 이벤트가 실행된다. 버튼에 설정된 addEventListener 의 세번째 인자로 false 를 적용하였으므로 이벤트 버블링이 일어난다. 그러므로 body 까지 올라가면서 클릭 이벤트가 전달된다. 이 과정에서 parent 는 캡쳐링만 적용되기 때문에 더이상 이벤트 핸들러 함수가 실행되지 않는다. body 는 이벤트 버블링이 적용되므로 클릭 이벤트가 실행된다.
만약 아래와 같이 parent 에 click 이벤트가 아닌 다른 이벤트를 설정하면 어떻게 될까?
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function clickbody(){
console.log('body clicked !')
}
function hideParent(){
console.log('parent clicked !')
// parent.classList.add('hide')
}
function popup(){
console.log('child clicked !')
}
parent.addEventListener('change', hideParent, false)
child.addEventListener('click', popup, false)
document.body.addEventListener('click', clickbody, false)
addEventListener 의 세번째 인자로 모두 false 를 설정하였으므로 이벤트 버블링이 일어난다. 이때 버튼을 클릭하면 클릭한 버튼에서부터 body 까지 올라가면서 클릭 이벤트가 전달된다. 하지만 parent 는 클릭 이벤트가 아니기 때문에 hideParent 함수는 실행되지 않고 body 의 클릭 이벤트에 등록된 핸들러 함수만 실행된다.
* 이벤트 캡쳐링과 버블링 방지하기 - e.stopPropagation 메서드
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function hideParent(){
console.log('parent clicked !')
parent.classList.add('hide')
}
function popup(){
alert('button clicked !')
}
parent.addEventListener('click', hideParent)
child.addEventListener('click', popup)
app.js 파일을 위와 같이 수정하자!
button 을 클릭하면 button 에 등록된 이벤트핸들러 함수가 실행된 다음 부모요소에 등록된 이벤트핸들러 함수가 실행된다. 즉, 이벤트 버블링으로 동작한다. 왜냐하면 addEventListener 의 세번째 인자에 디폴트값으로 false 가 설정되어 있기 때문이다.
그렇다면 button 이 클릭될때 이벤트 버블링을 방지해서 부모요소가 사라지지 않도록 하려면 어떻게 하면 될까?
const parent = document.querySelector('.parent')
const child = document.getElementById('child')
function hideParent(){
console.log('parent clicked !')
parent.classList.add('hide')
}
function popup(e){
alert('button clicked !')
e.stopPropagation() // 이벤트 버블링 방지
}
parent.addEventListener('click', hideParent)
child.addEventListener('click', popup)
위와 같이 이벤트 객체(e) 의 stopPropagation 메서드를 사용하면 된다. 이렇게 하면 자식요소인 button 에 등록된 이벤트 핸들러 함수만 실행되고, 부모요소에 등록된 이벤트핸들러 함수는 더이상 실행되지 않는다. 즉, 이벤트 버블링이 동작하지 않게 된다.
* 이벤트 위임 (Event delegation)
이벤트 버블링을 이용하여 부모 요소에 이벤트 핸들러 함수를 등록해놓고, 자식 요소에 이벤트 발생시 이벤트가 버블링되면서 부모 요소까지 전달되면 부모에 등록된 이벤트 핸들러 함수가 실행된다.
이벤트 위임은 자식 요소에 각각 이벤트를 등록하지 않고 부모 요소에 이벤트를 하나만 등록하는 방식을 의미한다. 말 그대로 부모요소에 이벤트를 위임하는 것이다. 이벤트 위임은 주로 리스트에 아이템 수가 많을때 사용된다. 왜냐하면 앞으로 리스트에 아이템이 몇개가 더 추가될지 예상할 수 없기 때문이다.
말로 설명하려면 힘드니까 일단 코드를 동작시켜 보면서 이해하도록 하자!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>좋아하는 과일</h1>
<div id='fruits'>
<div class='fruit'>사과</div>
<div class='fruit'>바나나</div>
<div class='fruit'>오렌지</div>
<div class='fruit'>딸기</div>
<div class='fruit'>수박</div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일을 위와 같이 작성하자! 내가 좋아하는 과일의 리스트를 화면에 보여준다.
body{
margin: 0;
padding: 0;
text-align: center;
}
#fruits{
width: 50%;
margin: auto;
}
.fruit{
width: 100%;
height: 50px;
line-height: 50px;
margin-top: 20px;
background: peru;
color: white;
font-weight: bold;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.7s;
}
style.css 파일을 위와 같이 작성하자!
const fruits = document.querySelectorAll('.fruit')
let prevTarget = null
function moveToRight(e){
// 바로 전에 클릭한 타겟의 위치 초기화
if(prevTarget !== null){
prevTarget.style.marginLeft = '0px'
}
// 현재 클릭한 타겟의 위치 변경
const target = e.target
target.style.marginLeft = '100px'
// 현재 타겟 저장
prevTarget = target
}
for(let fruit of fruits){
fruit.addEventListener('click', moveToRight)
}
app.js 파일을 위와 같이 작성하자!
const fruits = document.querySelectorAll('.fruit')
클래스명이 fruit 인 모든 요소를 검색한다.
for(let fruit of fruits){
fruit.addEventListener('click', moveToRight)
}
반복문을 순회하면서 리스트 아이템 각각에 click 이벤트를 등록한다.
let prevTarget = null
바로 전에 클릭한 타겟이 뭔지 알아내기 위하여 위와 같은 변수를 선언한다.
function moveToRight(e){
// 바로 전에 클릭한 타겟의 위치 초기화
if(prevTarget !== null){
prevTarget.style.marginLeft = '0px'
}
// 현재 클릭한 타겟의 위치 변경
const target = e.target
target.style.marginLeft = '100px'
// 현재 타겟 저장
prevTarget = target
}
moveToRight 은 리스트의 각 아이템이 클릭될때 실행되는 이벤트핸들러 함수이다. 핸들러 함수가 실행되면 바로 전에 클릭한 타겟의 위치를 초기화한다. 즉, marginLeft 속성을 0px 로 설정한다. 그런 다음 현재 사용자가 클릭한 아이템의 위치만 오른쪽으로 100px 만큼 이동시킨다. 결과는 아래와 같이 동작한다.
하지만 이렇게 하면 중대한 문제가 생긴다. 만약 리스트의 아이템이 1000개, 10000개, 1억개이면 어떻게 할 것인가? 이런 경우에도 반복문을 사용해서 모든 아이템에 각각 click 이벤트를 등록해줄 것인가? 그렇게 하면 아이템이 추가될때마다 이벤트핸들러를 따로 등록해줘야 하는 번거로움도 발생한다.
아래와 같이 리스트 아이템들을 감싸고 있는 부모요소에만 이벤트를 등록하면 문제가 해결이 된다. 이벤트 위임은 이벤트 버블링 때문에 적용 가능한 기술이다. 즉, 자식요소인 각각의 아이템을 클릭하더라도 이벤트 버블링에 의하여 부모요소에도 이벤트가 전파되기 때문이다.
const fruitContainer = document.getElementById('fruits')
let prevTarget = null
function moveToRight(e){
// 바로 전에 클릭한 타겟의 위치 초기화
if(prevTarget !== null){
prevTarget.style.marginLeft = '0px'
}
// 현재 클릭한 타겟의 위치 변경
const target = e.target
if(target.className === 'fruit'){
target.style.marginLeft = '100px'
// 현재 타겟 저장
prevTarget = target
}
}
fruitContainer.addEventListener('click', moveToRight)
app.js 파일을 위와 같이 수정하자!
const fruitContainer = document.getElementById('fruits')
리스트 아이템들을 감싸고 있는 부모요소인 fruitContainer 를 검색한다.
fruitContainer.addEventListener('click', moveToRight)
fruitContainer 에 click 이벤트를 등록하고, moveToRight 이라는 이벤트핸들러 함수를 연결한다.
let prevTarget = null
바로 전에 클릭한 타겟이 뭔지 알아내기 위하여 위와 같은 변수를 선언한다.
// 바로 전에 클릭한 타겟의 위치 초기화
if(prevTarget !== null){
prevTarget.style.marginLeft = '0px'
}
바로 전에 클릭한 타겟의 위치를 초기화한다. 즉, 원래 위치로 이동시킨다.
// 현재 클릭한 타겟의 위치 변경
const target = e.target
if(target.className === 'fruit'){
target.style.marginLeft = '100px'
// 현재 타겟 저장
prevTarget = target
}
현재 클릭한 타겟은 리스트 각각의 아이템이 될 수도 있지만, 그것들을 감싸고 있는 부모요소일 수도 있다. 그러므로 조건문을 사용해서 클릭한 타겟의 클래스명이 fruit 이면 해당 타겟의 위치를 오른쪽으로 100px 만큼 이동시킨다. 즉, 리스트 아이템 각각에 click 이벤트를 등록하는 대신 부모요소에만 click 이벤트를 등록하고, 조건문으로 리스트 아이템을 클릭한 경우만 위치를 이동시키도록 필터링해주었다.
// 현재 타겟 저장
prevTarget = target
다음 이벤트 발생시 바로 전에 클릭한 타겟이 뭔지 알기 위하여 현재 타겟을 저장해둔다.
* 브라우저 데이터 저장(localStorage, sessionStorage)
JSON 객체 (객체의 JSON변경)
폼(Form) 조작
alert, prompt, confirm
history, location, navigator 객체
* 이벤트 처리하기 연습문제 1
버튼을 클릭했을때 아래와 같이 타일의 색상을 랜덤으로 변경하는 프로그램을 완성해보자!
Math.floor(Math.random()*256)
자바스크립에서 랜덤숫자를 생성할때 Math.random() 메서드를 사용한다. 위 코드는 0~255 사이의 정수를 랜덤으로 선택한다.
rgb(빨간색, 초록색 , 파란색)
css 에서 색상을 지정할때 위와 같은 함수를 사용할 수 있다. rgb 함수의 인자로 순서대로 빨간색, 초록색, 파란색 컬러를 0 ~255 사이의 숫자로 설정하면 된다. 예를 들면, rgb(0, 255, 0) 는 초록색을 의미한다.
element.style.background = `rgb(${빨간색} , ${초록색}, ${파란색})`
자바스크립에서 스타일을 적용할때는 속성값을 항상 문자열로 설정을 해줘야 한다.
* 이벤트 처리하기 연습문제 2
setTimeout 메서드를 사용해서 화면에 사진이 1초 뒤에 나타났다가 3초 뒤에 사라지게 해보자!
* 이벤트 처리하기 연습문제 3
setInterval 메서드를 사용해서 화면에 1초마다 숫자가 카운팅되도록 해보자!
* 이벤트 처리하기 연습문제 4
setInterval 메서드를 사용해서 화면에 1초마다 문장의 단어 한글자씩 디스플레이되도록 해보자!
const text = 'You are watching text now !'
자바스크립트에서 문자열도 배열이다. 각각의 문자(character) 에 접근하려면 배열의 인덱스로 접근하면 된다. 만약 u 라는 문자를 조회하려면 text[2] 로 조회하면 된다.
const timerId = setInterval(함수, 시간간격)
clearInterval(timerId)
setInterval 함수는 타이머 ID 를 반환한다. 만약 문자열에서 각 문자를 조회할때 배열의 인덱스를 벗어나면 clearInterval 함수를 사용해서 타어머를 해제해주면 함수 실행이 멈춘다.
* 이벤트 처리하기 연습문제 5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel='stylesheet' href='style.css'/>
</head>
<body>
<script src='app.js'></script>
</body>
</html>
.circle{
width: 100px;
height: 100px;
border-radius: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: peru;
background-image: url('super-mario-left.jpg');
background-repeat: no-repeat;
background-size: cover;
}
주어진 HTML, CSS 코드를 사용하여 마우스가 클릭될때마다 아래와 같이 마우스 포인트 위치에 똑같은 이미지가 추가될 수 있도록 해보자! 이미지는 아무거나 사용해도 된다.
* 이벤트 처리하기 연습문제 6
리스트 만들기 (포트폴리오 프로젝트)에 커스텀 커서를 적용해보자!
* 이벤트 처리하기 연습문제 7
리스트 만들기 (포트폴리오 프로젝트)에서 버튼이 클릭될때 모달창이 열리도록 해보자!
* 이벤트 처리하기 연습과제 8
리스트 만들기 (포트폴리오 프로젝트)에서 모달창의 Close 버튼이 클릭될때 모달창이 닫히게 해보자!
* 이벤트 처리하기 연습과제 9
리스트 만들기 (포트폴리오 프로젝트)에서 스크린 모드 버튼을 클릭할때 버튼 내부의 원이 오른쪽으로 이동하게 해보자!
* 이벤트 처리하기 연습과제 10
리스트 만들기 (포트폴리오 프로젝트)에서 스크린 모드 버튼을 클릭할때마다 모드가 변경되게 해보자! 스크린 모드 버튼을 클릭할때마다 주간모드에서 야간모드로 변경되고, 야간모드에서 주간모드로 변경된다. 주간 모드는 현재 프로젝트의 스타일 그대로 두면 되고, 야간모드는 전체 color 를 흰색으로 하고, backgrond 를 검은색으로 하면 된다.
* 이벤트 처리하기 연습문제 11
사이드바 메뉴 예제코드를 이용해서 버튼 클릭시 아래와 같은 사이드바 메뉴가 나타나게 하고 setTimeout 메서드를 적용해서 3초 뒤에 사이드바 메뉴가 사라지게 해보자!
Element.classList.remove('클래스명')
요소의 클래스명을 제거하는 코드는 위와 같다. 예제를 완성하는데 해당 브라우저 API 를 활용한다.
* 이벤트 처리하기 연습문제 12
커로셀 예제를 이용하여 특정 셀렉터를 클릭하면 해당 셀렉터의 사진을 보여주고 셀렉터의 모양도 변경되도록 해보자!
* 이벤트 처리하기 연습문제 13
주어진 코드를 사용하여 아래와 같은 로딩화면을 구현해보자! 원이 순서대로 오르락 내리락 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>자바스크립트 연습</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="box-container">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
<script src='app.js'></script>
</body>
</html>
index.html 파일은 위와 같다.
body{
margin: 0;
padding: 0;
text-align: center;
}
#box-container{
width: 100%;
height: 100vh;
background-color: lightsalmon;
display: flex;
justify-content: center;
align-items: center;
}
.box{
width: 30px;
height: 30px;
border-radius: 50%;
background-color: lightpink;
transition: all 0.5s;
margin-left: 20px;
user-select: none;
}
style.css 파일은 위와 같다.
* 이벤트 처리하기 연습문제 14
아코디언 예제코드에서 특정 메뉴를 선택하면 해당 메뉴에 대한 정보만 보여지고 나머지 메뉴에 대한 정보는 닫히도록 해보자!
이벤트 처리하기 연습과제 15
페이지네이션 버튼 예제코드에서 이전, 다음 버튼을 양옆에 추가하고, 그 사이에는 페이지네이션 버튼 5개만 보여주도록 한다. 만약 초기 로딩시 페이지네이션 버튼의 숫자가 1, 2, 3, 4, 5 라면 다음 버튼을 클릭한 경우 버튼 숫자는 6, 7, 8, 9, 10 이 된다.
(단, 첫페이지에는 이전 버튼이 없다. 마지막 페이지에는 다음 버튼이 없다. )
'프론트엔드 > Javascript' 카테고리의 다른 글
자바스크립트 문법 5 - 이벤트(Event) 처리하기 3 (0) | 2021.12.23 |
---|---|
자바스크립트 문법 4 - 이벤트 (Event) 처리하기 2 (0) | 2021.12.19 |
에러 처리 (Error handling) (0) | 2021.10.09 |
자바스크립트 문법 14 - 비동기 처리 (Asynchronouse) (0) | 2021.10.09 |
자바스크립트 문법 2 - 브라우저에서의 자바스크립트 (0) | 2021.10.09 |