프론트엔드/Javascript

자바스크립트 문법 5 - 이벤트(Event) 처리하기 3

syleemomo 2021. 12. 23. 03:09
728x90

 

* 스크롤 이벤트

스크롤 이벤트는 마우스 휠을 스크롤할때 발생하는 이벤트이다. 해당 이벤트를 사용하면 웹페이지에서 유용한 효과나 애니메이션을 보여줄 수 있다. 

 

* 가로 스크롤링

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
  
</head>
<body>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
      <div class="item">4</div>
      <div class="item">5</div>
      <div class="item">6</div>
      <div class="item">7</div>
      <div class="item">8</div>
      <div class="item">9</div>
      <div class="item">10</div>
      <div class="item">11</div>
      <div class="item">12</div>
      <div class="item">13</div>
      <div class="item">14</div>
      <div class="item">15</div>
      <div class="item">16</div>
      <div class="item">17</div>
      <div class="item">18</div>
      <div class="item">19</div>
      <div class="item">20</div>
    </div>
  <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성한다. 리스트 아이템이 20개가 있다. 

body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
}
.container{
  display: flex;
  width: 80%; height: 200px;
  margin: 100px auto;
  overflow-x: auto;
}
.container .item{
  width: 200px;
  height: 100%;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  flex: 1 0 auto; /* flex-shrink 때문에 박스가 줄어드는것을 방지함 */
  text-align: center;
  line-height: 200px;
  font-size: 5rem;
  color: #fff;
  cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}

스타일을 위와 같이 작성한다. 

const container = document.querySelector('.container')

function scrollToItem(e){
  if(e.target.classList.contains('item')){
    console.log(e.target)
    e.target.scrollIntoView({ behavior: "smooth", inline: "center" })
  }
}
container.addEventListener('click', scrollToItem)

app.js 파일을 위와 같이 작성한다. 

가로 스크롤링

요소의 scrollIntoView 메서드를 사용하면 해당 요소를 클릭할때 스크롤이 되면서 해당 요소를 특정 위치로 이동한다. 현재는 inline 속성을 "center"로 설정하였기 때문에 클릭시 스크롤이 되면서 해당요소가 스크롤 위치에서 중앙으로 이동하도록 한다. 

 

 

* 가로 스크롤링(세로방향 스크롤시)

세로방향 스크롤시 가로방향으로 배치된 리스트가 가로로 스크롤링되도록 해보자!

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
  
</head>
<body>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
      <div class="item">4</div>
      <div class="item">5</div>
      <div class="item">6</div>
      <div class="item">7</div>
      <div class="item">8</div>
      <div class="item">9</div>
      <div class="item">10</div>
      <div class="item">11</div>
      <div class="item">12</div>
      <div class="item">13</div>
      <div class="item">14</div>
      <div class="item">15</div>
      <div class="item">16</div>
      <div class="item">17</div>
      <div class="item">18</div>
      <div class="item">19</div>
      <div class="item">20</div>
    </div>
  <script src="app.js"></script>
</body>
</html>

index.html 파일은 변경사항이 없다. 

body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
  height: 300vh;
}
.container{
  display: flex;
  width: 80%; height: 200px;
  overflow-x: auto;
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
}
.container .item{
  width: 200px;
  height: 100%;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  flex: 1 0 auto; /* flex-shrink 때문에 박스가 줄어드는것을 방지함 */
  text-align: center;
  line-height: 200px;
  font-size: 5rem;
  color: #fff;
  cursor: pointer;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}

style.css 파일을 위와 같이 수정하자!

body{
    background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
    height: 300vh; /* 추가 */
}

body 태그에 높이를 브라우저 높이보다 크게 설정해서 스크롤이 될 수 있도록 한다.

.container{
    display: flex;
    width: 80%; height: 200px;
    overflow-x: auto;
    position: fixed;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
}

리스트 목록을 감싸고 있는 컨테이너는 스크롤시에도 브라우저 화면 중앙에 고정되도록 한다.

const container = document.querySelector('.container')
const clientHeight = document.documentElement.clientHeight
let lastScrollLocation = 0 // 최근 스크롤 위치 기억하기
let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

function scrollHorizontally(){
  console.log(window.pageYOffset / (scrollHeight - clientHeight))
  container.scrollLeft = window.pageYOffset / (scrollHeight - clientHeight) * (container.scrollWidth - container.clientWidth)
}
window.addEventListener('scroll', scrollHorizontally)

scrollHeight 은 크로스 브라우징을 위하여 위와 같이 조회한다. window.pageYOffset 은 스크롤링하면서 이동한 거리이다. (scrollHeight - clientHeight) 은 세로방향의 스크롤 범위이다. 그러므로 window.pageYOffset / (scrollHeight - clientHeight) 은 스크롤링할때 세로방향으로 이동한 비율이다. (container.scrollWidth - container.clientWidth) 는 가로방향의 스크롤 범위이다. 가로방향의 스크롤 범위에 세로방향으로 스크롤링한 비율만큼 곱해서 가로방향으로 얼마만큼 이동할지를 결정한다. 예를 들어 세로방향으로 스크롤링한 비율이 절반(1/2)이면 가로방향의 스크롤 범위(200px) * 절반(1/2) 인 100px 만큼 가로방향으로 스크롤하면 된다. 

 

서큘러

서큘러는 세로방향으로 스크롤시 원(circle)이 회전하는 형태이다.

circlular

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
  
</head>
<body>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
      <div class="item">4</div>
      <div class="item">5</div>
      <div class="item">6</div>
      <div class="item">7</div>
      <div class="item">8</div>
      <div class="item">9</div>
      <div class="item">10</div>
      <div class="item">11</div>
      <div class="item">12</div>
      <div class="item">13</div>
      <div class="item">14</div>
      <div class="item">15</div>
      <div class="item">16</div>
      <div class="item">17</div>
      <div class="item">18</div>
      <div class="item">19</div>
      <div class="item">20</div>
    </div>
  <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자! 앞선 예제와 동일하다.

body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
  height: 300vh;
  scroll-behavior: smooth;
}
.container{
  width: 30vw; height: 30vw;
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  // border: 3px solid red;
  transition: .2s;
}
.container .item{
  width: 100px;
  height: 100px;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  line-height: 100px;
  font-size: 2rem;
  text-align: center;
  color: #fff;
  cursor: pointer;
  position: fixed;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox * transform: translate(-50%, -50%); */
}

style.css 파일을 위와 같이 작성하자! 

css 가 적용된 모습

 

이제 item 클래스가 적용된 요소들의 위치는 left, top 을 동적으로 설정하여 원(circle) 형태가 되게 한다.

const container = document.querySelector('.container')
const items = document.querySelectorAll('.item')
const coords = container.getBoundingClientRect()
const clientHeight = document.documentElement.clientHeight
const xcenter = coords.width / 2 
const ycenter = coords.height / 2 
const Radius = 350

let lastScrollLocation = 0 // 최근 스크롤 위치 기억하기
let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

function scrollHorizontally(){
  container.style.transform = `translate(-50%, -50%) rotate(${window.pageYOffset / (scrollHeight - clientHeight) * 360}deg)`
}
function degToRad(deg){
  return deg * (Math.PI/180)
}
function setPosition(xc, yc, R, delta){
  const radian = degToRad(delta)
  return [R*Math.cos(radian) + xc, R*Math.sin(radian) + yc]
}
window.addEventListener('scroll', scrollHorizontally)

for(let i = 0; i < items.length; i++){
  console.log(360/items.length * (i+1), xcenter, ycenter)
  const [x, y] = setPosition(xcenter, ycenter, Radius, 360/items.length * (i+1))
  items[i].style.left = `${x}px`
  items[i].style.top = `${y}px`
}

app.js 파일을 위와 같이 작성하자!

const container = document.querySelector('.container')

컨테이너를 회전시키기 위하여 요소를 검색한다.

const items = document.querySelectorAll('.item')

웹 화면 로딩시 리스트 아이템을 화면에 배치하기 위하여 요소를 검색한다. 

const coords = container.getBoundingClientRect()
const xcenter = coords.width / 2
const ycenter = coords.height / 2

컨테이너의 너비와 높이를 알아내기 위하여 요소의 getBoundingClientRect 메서드를 이용한다. 컨테이너 중앙지점에서 리스트 아이템을 원(circle)의 형태로 배치하기 위하여 컨테이너 중앙지점의 위치(xcenter, ycenter)도 함께 계산한다.

const Radius = 350

원(circle)의 반지름을 설정한다. 숫자가 클수록 원의 크기가 커진다.

const clientHeight = document.documentElement.clientHeight

브라우저 높이를 조회한다.

let scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

문서높이를 조회한다. 문서높이는 브라우저에 보이지 않는 부분까지 포함한 높이이다. 

for(let i = 0; i < items.length; i++){
    console.log(360/items.length * (i+1), xcenter, ycenter)
    const [x, y] = setPosition(xcenter, ycenter, Radius, 360/items.length * (i+1))
    items[i].style.left = `${x}px`
    items[i].style.top = `${y}px`
}

리스트의 아이템 갯수만큼 반복하면서 원의 형태로 아이템을 배치한다. 360/items.length * (i+1)은 리스트 아이템의 (x, y) 좌표를 계산하기 위한 각도이다. items.length 는 20이므로 360/20 = 18도이다. 즉, 18도씩 증가하면서 아이템의 (x, y) 위치를 계산한다. 

function degToRad(deg){
    return deg * (Math.PI/180)
  }
  function setPosition(xc, yc, R, delta){
    const radian = degToRad(delta)
    return [R*Math.cos(radian) + xc, R*Math.sin(radian) + yc]
  }

setPosition 함수는 원의 중심, 반지름, 각도를 파라미터로 받아서 리스트 아이템 각각의 (x, y)좌표를 계산한다. 이때 delta (각도)는 degree 단위이므로 라디안(radian)으로 변경이 필요하다. 이때 Math.cos, Math.sin 함수를 이용하여 코사인, 사인을 계산한다. 

function scrollHorizontally(){
    container.style.transform = `translate(-50%, -50%) rotate(${window.pageYOffset / (scrollHeight - clientHeight) * 360}deg)`
}

window.pageYOffset / (scrollHeight - clientHeight)은 앞서 설명한것처럼 세로방향으로 스크롤한 비율이다. 해당 비율만큼 컨테이너를 회전시켜주면 세로방향 스크롤을 회전으로 전환할 수 있다.

 

화면 리사이즈시 아이템 위치변경

body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
  height: 300vh;
  scroll-behavior: smooth;
}
.container{
  width: 70vw; height: 70vw;
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  border: 3px solid red;
  border-radius: 50%;
  transition: .2s;
}
.container .item{
  width: 100px;
  height: 100px;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  line-height: 100px;
  font-size: 2rem;
  text-align: center;
  color: #fff;
  cursor: pointer;
  position: fixed;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox * transform: translate(-50%, -50%); */
}

style.css 파일은 거의 동일하다. container 요소의 너비와 높이만 조금 수정하였다. 

const container = document.querySelector('.container')
const items = document.querySelectorAll('.item')
let Radius = 250, xcenter, ycenter, coords
const clientHeight = document.documentElement.clientHeight
const scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

function degToRad(deg){
  return deg * (Math.PI / 180) // 라디안값 
}
function setPosition(xc, yc, R, delta){
  const radian = degToRad(delta)
  const x = R*Math.cos(radian) + xc 
  const y = R*Math.sin(radian) + yc 
  return [x, y]
}

function circleItems(){
  const [xcenter, ycenter] = changeCenter()
  console.log(xcenter, ycenter, 
    coords.left, coords.right,
    coords.top, coords.bottom)
  for(let i=0 ; i<items.length; i++){
    const [x, y] = setPosition(xcenter, ycenter, Radius, 360/items.length * (i+1))
    items[i].style.left = `${x}px`
    items[i].style.top = `${y}px`
    // items[i].style.transform = 'translate(-50%, -50%)'
  }
}
function scrollCircular(){
  const theta = window.pageYOffset / (scrollHeight - clientHeight) * 360
  container.style.transform = `translate(-50%, -50%) rotate(${theta}deg)`
}
function changeCenter(){
  coords = container.getBoundingClientRect()
  xcenter = (coords.right - coords.left) / 2
  ycenter = (coords.bottom - coords.top) / 2
  return [xcenter, ycenter]
}
function getMousePosition(e){
  console.log(e.clientX, e.clientY)
}
circleItems()

window.addEventListener('scroll', scrollCircular)
window.addEventListener('mousedown', getMousePosition)
window.addEventListener('resize', circleItems)

app.js 파일을 위와 같이 수정하자!

window.addEventListener('resize', circleItems)

window 객체의 resize 이벤트는 브라우저의 크기가 변경될때 발생한다. 이때 원(circle) 형태로 배치된 아이템의 위치를 다시 조정해주면 된다. 

function circleItems(){
  const [xcenter, ycenter] = changeCenter()
  for(let i = 0; i < items.length; i++){
    console.log(360/items.length * (i+1), xcenter, ycenter)
    const [x, y] = setPosition(xcenter, ycenter, Radius, 360/items.length * (i+1))
    items[i].style.left = `${x}px`
    items[i].style.top = `${y}px`
  }
}

circleItems 이벤트핸들러 함수는 브라우저의 크기가 변경될때 실행된다. 이때 원의 중심도 이동하기 때문에 changeCenter 함수로 원의 중심을 다시 계산한다. 또한, 반복문을 실행하면서 원(circle)에 배치된 각 아이템의 위치도 다시 계산하고, 배치한다. 

function changeCenter(){
  coords = container.getBoundingClientRect()
  xcenter = coords.width / 2
  ycenter = coords.height / 2
  return [xcenter, ycenter]
}

원(circle)의 중심을 구하는 코드블럭이다. 먼저 요소의 getBoundingClientRect 함수를 이용하여 요소의 크기와 위치에 대한 정보를 조회한다. 요소너비의 절반과 요소높이의 절반을 원의 중심으로 설정한다.

function changeCenter(){
  coords = container.getBoundingClientRect()
  xcenter = (coords.right - coords.left) / 2
  ycenter = (coords.bottom - coords.top) / 2
  return [xcenter, ycenter]
}

또는 위와 같이 요소의 위치좌표를 이용하여 원의 중심을 구해도 동일하게 동작한다.

circleItems()

브라우저 크기가 변경될때도 아이템의 위치를 다시 계산하지만, 웹 화면이 처음 로딩될때도 아이템 위치를 계산해서 화면에 보여줘야 한다. 

let xcenter
let ycenter 
let coords

원의 중심과 container 요소의 위치/크기 정보는 브라우저 크기가 변경될때마다 새로 구해야 하므로 굳이 초기값을 설정하지 않아도 된다.

 

https://iborymagic.tistory.com/52

 

Click and Drag to Scroll

웹서핑을 하다보면, 마우스로 드래그해서 스크롤을 할 수 있게끔 해 놓은 페이지들을 종종 볼 수 있다. 마치 터치를 하는 것과 비슷한 조작감을 주곤 하는데, 오늘은 이 드래그 스크롤을 구현해

iborymagic.tistory.com

* 가로스크롤링 (마우스 드래그앤드롭)

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
  
</head>
<body>
    <div class="container">
      <div class="item">1</div>
      <div class="item">2</div>
      <div class="item">3</div>
      <div class="item">4</div>
      <div class="item">5</div>
      <div class="item">6</div>
      <div class="item">7</div>
      <div class="item">8</div>
      <div class="item">9</div>
      <div class="item">10</div>
      <div class="item">11</div>
      <div class="item">12</div>
      <div class="item">13</div>
      <div class="item">14</div>
      <div class="item">15</div>
      <div class="item">16</div>
      <div class="item">17</div>
      <div class="item">18</div>
      <div class="item">19</div>
      <div class="item">20</div>
    </div>
  <script src="app.js"></script>
</body>
</html>

index.html 파일은 앞선 수업예제에서 가로 스크롤링 예제와 동일하다. 

body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
}
.container{
  display: flex;
  width: 80%; height: 200px;
  margin: 100px auto;
  overflow-x: auto;
}
.container .item{
  width: 200px;
  height: 100%;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  flex: 1 0 auto; /* flex-shrink 때문에 박스가 줄어드는것을 방지함 */
  text-align: center;
  line-height: 200px;
  font-size: 5rem;
  color: #fff;
  cursor: pointer;
  transition: .3s;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}
.active{
  transform: scale(1.02);
}

style.css 파일을 위와 같이 작성하자! 앞선 수업예제에서 가로 스크롤링 예제와 유사하지만, 아이템이 액티브인 경우 적용할 active 클래스 정의가 추가되었다. 

const slider = document.querySelector('.container');
let isDown = false;
let startX;
let scrollLeft;

slider.addEventListener('mousedown', e => {
  isDown = true;
  slider.classList.add('active');
  startX = e.pageX - slider.offsetLeft;
  scrollLeft = slider.scrollLeft;
});

slider.addEventListener('mouseleave', () => {
  isDown = false;
  slider.classList.remove('active');
});

slider.addEventListener('mouseup', () => {
  isDown = false;
  slider.classList.remove('active');
});

slider.addEventListener('mousemove', e => {
  if (!isDown) return; 
  e.preventDefault();
  const x = e.pageX - slider.offsetLeft;
  const walk = x - startX;
  slider.scrollLeft = scrollLeft - walk;
});

app.js 파일을 위와 같이 작성하자!

기본적인 원리는 마우스 클릭시 컨테이너 안에서 마우스 클릭지점(startX)과 클릭시 스크롤 위치(scrollLeft)를 저장한다. 마우스를 드래그하면 마우스가 이동한 거리(walk)만큼 이전 스크롤 위치(scrollLeft)에서 빼준다.

마우스를 왼쪽으로 움직이면 다음 아이템을 보여주기 위하여 스크롤은 클릭지점에서 더 증가해야 한다. 반대로 마우스를 오른쪽으로 움직이면 이전 아이템을 보여주기 위하여 스크롤은 클릭지점에서 더 감소해야 한다.

마우스가 왼쪽으로 움직일때 walk 은 음수값이고, 오른쪽으로 움직일때 walk 은 양수값이다. 그래서 마우스가 왼쪽으로 움직일때는 스크롤 위치가 증가해야 하므로 빼주고, 오른쪽으로 움직일때는 스크롤 위치가 감소해야 하므로 이때도 빼준다. 

const container = document.querySelector('.container')
let isDown = false // 플래그 : 현재 마우스 클릭여부 판단 
let startX // 마우스 클릭시 마우스의 X 좌표 
let scrollLeft // 최근 스크롤바 위치 저장 

container.addEventListener('mousedown', e => {
  isDown = true 
  container.classList.add('active')
  // 컨테이너 기준 클릭한 마우스의 x 좌표 
  startX = e.pageX - container.offsetLeft 
  // 현재 스크롤바 위치 저장 
  scrollLeft = container.scrollLeft 
})
function deactive(){
  isDown = false 
  container.classList.remove('active')
}
container.addEventListener('mouseleave', deactive)
container.addEventListener('mouseup', deactive)

container.addEventListener('mousemove', e => {
  if(!isDown) return 
  e.preventDefault()
  // 마우스가 드래그할때 현재 마우스의 x 좌표 
  const x = e.pageX - container.offsetLeft 
  // 마우스 드래그 지점에서 이전에 마우스 클릭지점까지 이동한 거리
  const walk = x - startX
  // 최근 스크롤바 위치에서 마우스 이동거리만큼 더하거나 빼줌
  container.scrollLeft = scrollLeft - walk 
})

코드 주석을 첨가하면 위와 같다. container.offsetLeft 혹은 slider.offsetLeft 값은 사실 walk (마우스 드래그시 이동거리) 를 계산할때 x - startX 를 하면 상쇄되기 때문에 작성하지 않아도 동작한다. x와 startX 값 모두 해당 값을 포함하고 있기 때문이다.

 

* 가로 스크롤링 (아이템 너비만큼 이동)

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
  
</head>
<body>
    <div class="window">
      <button type="button" class="control before"><</button>
      <button type="button" class="control after">></button>
      <div class="container">
        <div class="item">1</div>
        <div class="item">2</div>
        <div class="item">3</div>
        <div class="item">4</div>
        <div class="item">5</div>
        <div class="item">6</div>
        <div class="item">7</div>
        <div class="item">8</div>
        <div class="item">9</div>
        <div class="item">10</div>
        <div class="item">11</div>
        <div class="item">12</div>
        <div class="item">13</div>
        <div class="item">14</div>
        <div class="item">15</div>
        <div class="item">16</div>
        <div class="item">17</div>
        <div class="item">18</div>
        <div class="item">19</div>
        <div class="item">20</div>
      </div>
    </div>
  <script src="app.js"></script>
</body>
</html>
body{
  background-image: url(https://images.pexels.com/photos/531880/pexels-photo-531880.jpeg?cs=srgb&dl=pexels-pixabay-531880.jpg&fm=jpg);
}
.window{
  width: 80%; height: 200px;
  margin: 100px auto;
  position: relative;
}
.container{
  display: flex;
  width: 100%; height: 200px;
  overflow-x: auto;
  scroll-behavior: smooth;
}
.control.before,
.control.after{
  width: 50px; 
  height: 50px;
  line-height: 50px;
  border-radius: 50%;
  text-align: center;
  position: absolute;
  top: 50%; transform: translateY(-50%);
  z-index: 1;
  font-weight: bold;
  font-size: 2rem;
  color: #fff;
  background-color: rgba(0, 0, 0, .3);
  cursor: pointer;
  border: none; outline: none;
  transition: .1s;
}
.control.before{
  left: -70px;
}
.control.after{
  content: '>';
  right: -70px;
}
.control.before:hover,
.control.after:hover{
  transform: translateY(-50%) scale(1.1);
  background-color: rgba(0, 0, 0, .5);
}
.container .item{
  width: 200px;
  height: 100%;
  margin-right: 10px;
  background-color: rgba(0, 0, 0, .2);
  backdrop-filter: blur(3px);
  flex: 1 0 auto; /* flex-shrink 때문에 박스가 줄어드는것을 방지함 */
  text-align: center;
  line-height: 200px;
  font-size: 5rem;
  color: #fff;
  cursor: pointer;
  transition: .3s;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.container {
  -ms-overflow-style: none;  /* IE and Edge */
  scrollbar-width: none;  /* Firefox */
}
.active{
  transform: scale(1.02);
}
const container = document.querySelector('.container')
const prevBtn = document.querySelector('.control.before')
const nextBtn = document.querySelector('.control.after')
const margin = 10
const delta = document.querySelector('.item').offsetWidth + margin // 아이템 너비 + 여백만큼씩 이동

function moveToLeft(){
    container.scrollLeft += delta // 다음 아이템을 보여주려면 가로 스크롤바가 오른쪽으로 이동해야 하므로 더해주기
}
function moveToRight(){
    container.scrollLeft -= delta // 이전 아이템을 보여주려면 가로 스크롤바가 왼쪽으로 이동해야 하므로 더해주기
}

prevBtn.addEventListener('click', moveToRight)
nextBtn.addEventListener('click', moveToLeft)

 

 

* 스크롤 애니메이션

참고문서

 

Element.getBoundingClientRect() - Web API | MDN

Element.getBoundingClientRect() 메서드는 엘리먼트의 크기와 뷰포트에 상대적인 위치 정보를 제공하는 DOMRect 객체를 반환합니다.

developer.mozilla.org

 

스크롤 애니메이션 효과
비디오 영상에 스크롤 애니메이션 적용

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <p>일본 애니메이션 역사에서 절대 빼 놓을 수 없는 존재로 자리매김한 스튜디오 지브리! 사하라 사막에 부는 열풍이란 뜻의 지브리는 일본을 넘어 이제는 세계 애니메이션에서도 대단한 인기를 끌고 있다. 애니메이션의 대가 미야자키 하야오를 중심으로 30여 년간 어른들도 동심에 빠져들게, 마음껏 상상의 나래를 펼칠 수 있는 주옥 같은 작품들을 만들어 내고 있다.</p>
  <div class="container">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
  <iframe width="1280" height="720" src="https://www.youtube.com/embed/DVLvpfeTJ7U" title="스즈메의 문단속 OST - 참새(すずめ) 한국어 커버(Korean Ver.) Ι RADWIMPS Ι Cover by 도도" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
  <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

body{
  margin: 0; padding: 0;
  background-color: #0e1111;

  display: flex;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  min-height: 400vh;
}
*{
  box-sizing: border-box;
}
.container{
  width: 40%;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  /* border: 1px solid wheat; */
  gap: 1rem;
}
p{
  width: 30%;
  font-size: 1.2rem;
  color: wheat;
  opacity: 0; transform: translateY(50px) scale(1.1);
  transition: 1s ease-in-out;
  /* border: 1px solid red; */
  margin: 3rem auto;
  text-align: center;
}
.container .item{
  width: 200px; height: 200px;
  min-width: 300px;
  background-size: cover;
  opacity: 0; transform: translateY(50px) scale(1.05); 
  transition: 1s ease-in-out;
  /* border: 1px solid red; */
}
iframe{
  opacity: 0; width: 60%; height: 720px; margin-top: 10rem;
  transition: 1s ease-in-out;
}
.item.fade{
  opacity: 1; transform: translateY(0) scale(1); 
}
p.fade{
  opacity: 1; transform: translateY(0) scale(1);
}
iframe.fade{
  opacity: 1; width: 70%; height: 800px; margin-top: 8rem; 
}
.container .item:nth-child(1){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRuIlg9172jMsDMgF9cppzLBbpMwTntLNgniQ&usqp=CAU') no-repeat center;
}
.container .item:nth-child(2){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ2GSftiaaV3uqt9eHukmHmxNU29R3LtbQvdg&usqp=CAU') no-repeat center;
}
.container .item:nth-child(3){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS4aNAibmpYUyJaHT_FLNwpPg9HE4WIv6nuOQID0bi7RG0d7WCXcbTF6qy8NCs4DgXpLTU&usqp=CAU') no-repeat center;
}
.container .item:nth-child(4){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSAwQAZ_qjz4tXPUQGQ1FB5RFz3PAyYPCozZw&usqp=CAU') no-repeat center;
}
.container .item:nth-child(5){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-sDZMwuIvgDzX37sVbe5_RxEofHFzx5kjTA&usqp=CAU') no-repeat center;
}
.container .item:nth-child(6){
  background: url('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQC3AoBjKswejqo8-k8ym64qU7WL8ympdT9kg&usqp=CAU') no-repeat center;
}

style.css 파일을 위와 같이 작성하자!

const container = document.querySelector('.container')
const items = container.querySelectorAll('.item')
const text = document.body.querySelector('p')
const video = document.querySelector('iframe')
const documentHeight = document.documentElement.clientHeight
let distFromBottom
let offset = -100
console.log(documentHeight)


window.addEventListener('scroll', (e) => {
  distFromBottom = documentHeight - text.getBoundingClientRect().bottom
  if(distFromBottom > offset){
    text.classList.add('fade')
  }
  items.forEach(function(item, index){
    distFromBottom = documentHeight - item.getBoundingClientRect().bottom
    if(distFromBottom > offset){
      console.log("인", index, item, item.getBoundingClientRect().bottom)
      item.classList.add('fade')
    }
  })
  distFromBottom = documentHeight - video.getBoundingClientRect().bottom
  if(distFromBottom > offset){
    video.classList.add('fade')
  }
})

app.js 파일을 위와 같이 작성하자!

const container = document.querySelector('.container')
const items = container.querySelectorAll('.item')
const text = document.body.querySelector('p')
const video = document.querySelector('iframe')

스크롤 애니메이션을 적용할 요소들을 조회한다.

const documentHeight = document.documentElement.clientHeight

브라우저의 높이이다. 

let distFromBottom

브라우저 하단으로부터 요소의 하단 모서리까지 얼마나 떨어져 있는지 거리를 저장한다.

let offset = -100

요소의 하단 모서리가 브라우저 하단보다 아래쪽에 있고, 거리가 100px 정도일때 애니메이션을 적용하기 위한 오프셋 값이다. 즉, 애니메이션을 적용할 시점을 정의한다.

window.addEventListener('scroll', (e) => {
  distFromBottom = documentHeight - text.getBoundingClientRect().bottom
  if(distFromBottom > offset){
    text.classList.add('fade')
  }
  items.forEach(function(item, index){
    distFromBottom = documentHeight - item.getBoundingClientRect().bottom
    if(distFromBottom > offset){
      console.log("인", index, item, item.getBoundingClientRect().bottom)
      item.classList.add('fade')
    }
  })
  distFromBottom = documentHeight - video.getBoundingClientRect().bottom
  if(distFromBottom > offset){
    video.classList.add('fade')
  }
})

스크롤 이벤트가 발생할때 실행되는 이벤트핸들러 함수이다.

distFromBottom = documentHeight - text.getBoundingClientRect().bottom

요소의 getBoundingClientRect().bottom 은 브라우저 상단으로부터 요소의 하단 모서리까지 거리를 계산한다. documentHeight 은 브라우저의 높이이므로 결국 distFromBottom 은 브라우저 하단으로부터 요소의 하단 모서리까지의 거리이다. 

if(distFromBottom > offset){
    text.classList.add('fade')
  }

요소의 하단 모서리가 브라우저 하단으로부터 아래쪽 100px 정도의 거리에 도달하면 텍스트에 fade 애니메이션을 적용한다.

items.forEach(function(item, index){
    distFromBottom = documentHeight - item.getBoundingClientRect().bottom
    if(distFromBottom > offset){
      console.log("인", index, item, item.getBoundingClientRect().bottom)
      item.classList.add('fade')
    }
  })

items 는 배열이므로 forEach 구문을 활용하여 코드블럭을 반복실행한다. 텍스트 애니메이션과 동일하게 리스트의 각 아이템에도 같은 조건에서 fade 애니메이션을 적용한다. 각 아이템의 하단 모서리가 브라우저 하단 아래쪽 100px 정도 거리에 가까워지면 애니메이션을 적용한다. 

distFromBottom = documentHeight - video.getBoundingClientRect().bottom
  if(distFromBottom > offset){
    video.classList.add('fade')
  }

비디오 영상에도 마찬가지로 애니메이션을 적용한다.

 

 

<!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='section'>
        <div class="contents up">content 1</div>
        <div class="contents up">content 2</div>
        <div class="contents up">content 3</div>
        <div class="contents up">content 4</div>
    </div>
    <script src="app.js"></script>
</body>

</html>

index.html 파일을 위와 같이 작성하자! 컨텐츠 4개가 있고, 이를 감싸는 섹션이 존재한다. 

body{
    height: 200vh;
}
#section{
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 400px;
}
.contents{
    background-color: brown;
    width: 300px;
    height: 300px;
    text-align: center;
    line-height: center;
    margin-right: 10px;
    opacity: 0;
    transition: all .5s ease;
}
.up{
    /* y 좌표를 아래쪽으로 100px 만큼 내린다 */
    transform: translate(0, 100px); 
}
.show{
    opacity: 1;
    transform: none;
}

style.css 파일을 위와 같이 작성하자! section div 에는 위쪽 마진을 400px 만큼 적용한다. 그래서 모든 컨텐츠는 처음에 웹페이지에서 400px 만큼 아래쪽에 위치한다. 거기에 더해서 up 이라는 클래스도 추가되어 있으므로 transform 속성이 translate(0, 100px) 로 설정되어 결과적으로 위쪽 마진은 500px 이 된다. 

const contents = document.querySelectorAll('.contents')

function startAnimation() {
    for (let content of contents) {
        console.log(content.getBoundingClientRect().top)
        if (!content.classList.contains('show') && content.getBoundingClientRect().top < 200) {
            content.classList.add('show')
        }
    }
}

window.addEventListener('scroll', startAnimation)

 app.js 파일을 위와 같이 작성하자! 

const contents = document.querySelectorAll('.contents')

클래스명이 contents 인 모든 요소를 검색한다. 

window.addEventListener('scroll', startAnimation)

window 객체에 scroll 이벤트를 등록한다. 즉, 웹페이지에서 스크롤이 되면 scroll 이벤트가 발생한다. scroll 이벤트가 발생할때 실행할 startAnimation 이벤트핸들러 함수를 연결한다. 

function startAnimation() {
    for (let content of contents) {
        console.log(content.getBoundingClientRect().top)
        if (!content.classList.contains('show') && content.getBoundingClientRect().top < 200) {
            content.classList.add('show')
        }
    }
}

클래스명이 contents 인 모든 div 요소를 조회하면서 show 라는 클래스명이 포함되어 있는지 검사한다. content.classList.contains('show') 부분이다. classList 객체의 contains 메서드는 해당 요소가 contains 의 인자로 전달된 클래스를 포함하는지 검사한다. 

content 요소가 아직 show 라는 클래스를 포함하고 있지 않으면, content 의 위쪽 마진이 200 px 보다 작아질때 show 클래스를 추가하면서 애니메이션 효과가 적용된다. show 클래스는 opacity 가 0 에서 1로 변하면서 컨텐츠가 화면에 나타나고, transform 이 translate(0, 100px) 에서 none 으로 변경되면서 위쪽으로 상승하는 것처럼 보여준다. 

 

이번에는 웹페이지에서 스크롤이 발생할때 컨텐츠가 내려가는 애니메이션 효과를 추가해보자!

<!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='section'>
        <div class="contents up">content 1</div>
        <div class="contents down">content 2</div>
        <div class="contents up">content 3</div>
        <div class="contents down">content 4</div>
    </div>
    <script src="app.js"></script>
</body>

</html>

index.html 파일을 위와 같이 수정하자! down 이라는 클래스명이 추가되었다.  

body{
    height: 200vh;
}
#section{
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 400px;
}
.contents{
    background-color: brown;
    width: 300px;
    height: 300px;
    text-align: center;
    line-height: center;
    margin-right: 10px;
    opacity: 0;
    transition: all .5s ease;
}
.up{
    /* y 좌표를 아래쪽으로 100px 만큼 내린다 */
    transform: translate(0, 100px); 
}
.down{
    /* y 좌표를 위쪽으로 100px 만큼 올린다 */
    transform: translate(0, -100px);
}
.show{
    opacity: 1;
    transform: none;
}

style.css 파일을 위와 같이 수정하자! down 이라는 클래스명이 추가되었다.

스크롤전 각 컨텐츠의 위치

content2 와 content4 는 기본 마진 400px 에서 -100px 이므로 결과적으로 300px 에 위치한다. content 의 위쪽 마진이 200px 일때 효과가 적용되므로 100px 만큼 남은 content2 와 content4 가 먼저 화면에 나타난다.화면에 나타나면서 아래쪽으로 내려가는 효과를 보여준다. content1 과 content3 은 위쪽 마진이 500px 부터 시작하므로 content2 와 content 4가 나타난 이후에 보여진다. 즉, 2번과 4번 컨텐츠가 먼저 아래쪽으로 내려가고, 1번과 3번 컨텐츠가 이후에 위로 올라가면서 애니메이션이 적용된다. 

 

이번에는 스크롤시 자연스럽게 슬라이드가 위로 올라가면서 화면이 전환되는 효과를 만들어보자!

스크롤시 슬라이드가 위로 올라가는 애니메이션 적용하기

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <section><p>1</p></section>
    <section><p>2</p></section>
    <section><p>3</p></section>
    <section><p>4</p></section>
    <section><p>5</p></section>
  </main>
  <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

body{
  margin: 0; padding: 0;
  box-sizing: border-box;
}
main{
  width: 100%;
}
main section{
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed; bottom: 0; left: 0; right: 0;
  transition: 1s ease-in-out;
}
main section p{
  font-size: 2rem;
  font-weight: bold;
  color: seashell;
  text-align: center;
  margin: 0; padding: 1rem 2rem;
  /* border: 1px solid red; */
}
main section:nth-child(1){
  background-color: yellowgreen;
}
main section:nth-child(2){
  background-color: brown;
  height: 0; opacity: 0;
}
main section:nth-child(3){
  background-color: yellow;
  height: 0; opacity: 0;
}
main section:nth-child(4){
  background-color: springgreen;
  height: 0; opacity: 0;
}
main section:nth-child(5){
  background-color: slateblue;
  height: 0; opacity: 0;
}

style.css 파일을 위와 같이 작성하자! 모든 섹션은 브라우저 크기만큼 차지하면서 브라우저 화면에 고정되도록 스타일링한다. 자연스러운 화면전환을 위하여 1초동안 트랜지션을 적용한다. 애니메이션 적용을 위하여 첫번째 슬라이드를 제외한 나머지 슬라이드의 높이와 투명도는 제로(0)으로 초기화한다. 

const main = document.querySelector('main')
const sections = main.querySelectorAll('section')
const clientHeight = document.documentElement.clientHeight // 브라우저 높이
const scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
)
const scrollRange = scrollHeight - clientHeight // 세로방향 스크롤 범위
const scrollRangeOfOneSection = scrollRange / (sections.length - 1) // 하나의 섹션에 대한 스크롤 범위
let index = 0, timer

// 1초동안 이벤트 금지하기 (자연스러운 슬라이드 효과 연출)
function trotthling(handler, e){
  if(!timer){
    timer = setTimeout(function(){
      handler(e)
      timer = null 
    }, 1000)
  }
}

function changeSlide(e){
  console.log('scroll', e.deltaY)
  
  if(e.deltaY > 0){ // 스크롤 내린 경우
    index++
    if(index > sections.length -1) index = 0
    console.log(index)
  }else{ // 스크롤 올린 경우
    index--
    if(index < 0) index = sections.length - 1
    console.log(index)
  }
  // // index 에 해당하는 섹션을 제외한 기존스타일 초기화
  // // 이전/다음 슬라이드로 모두 전환가능
  // for(let i=0; i<sections.length; i++){
  //     const section = sections[i]
  //     console.log(i, section)
  //     section.style.opacity = '0'
  //     section.style.height = '0'
    
  // }

  // 섹션에 애니메이션 적용하기 
  const section = sections[index]
  section.style.opacity = '1'
  section.style.height = '100vh'
}

document.addEventListener('wheel', (e) => trotthling(changeSlide, e))

app.js 파일을 위와 같이 작성하자!

const main = document.querySelector('main')
const sections = main.querySelectorAll('section')

섹션의 화면전환을 위하여 모든 섹션을 조회한다.

document.addEventListener('wheel', (e) => trotthling(changeSlide, e))

브라우저에 wheel 이벤트를 등록한다. 현재는 스크롤바가 없기 때문에 scroll 이벤트는 동작하지 않는다. 쓰로틀링을 적용하여 1초동안에는 wheel 이벤트가 동작하지 않도록 한다. 이렇게 하지 않으면 슬라이드가 너무 빠르게 넘어가기 때문이다.

// 1초동안 이벤트 금지하기 (자연스러운 슬라이드 효과 연출)
function trotthling(handler, e){
  if(!timer){
    timer = setTimeout(function(){
      handler(e)
      timer = null 
    }, 1000)
  }
}

trottling 함수는 이벤트핸들러 함수(handler)를 콜백으로 전달받는다. 또한, 이벤트 객체도 전달받는다. timer 변수가 null 이면 타이머를 생성한다. 타이머는 1초 후에 이벤트핸들러 함수를 실행하고, timer 변수를 null 로 초기화하여 다시 타이머를 생성할 수 있도록 한다. 즉, 타이머가 한번 생성되면 1초동안은 타이머가 새로 생성되지 않고, 이벤트핸들러 함수도 실행되지 않는다.

function changeSlide(e){
  console.log('scroll', e.deltaY)
  
  if(e.deltaY > 0){ // 스크롤 내린 경우
    index++
    if(index > sections.length -1) index = 0
    console.log(index)
  }else{ // 스크롤 올린 경우
    index--
    if(index < 0) index = sections.length - 1
    console.log(index)
  }
  // // index 에 해당하는 섹션을 제외한 기존스타일 초기화
  // // 이전/다음 슬라이드로 모두 전환가능
  // for(let i=0; i<sections.length; i++){
  //     const section = sections[i]
  //     console.log(i, section)
  //     section.style.opacity = '0'
  //     section.style.height = '0'
    
  // }

  // 섹션에 애니메이션 적용하기 
  const section = sections[index]
  section.style.opacity = '1'
  section.style.height = '100vh'
}

 마우스 휠을 올리거나 내릴때 실행되는 이벤트핸들러 함수이다. e.deltaY 는 스크롤을 내리는 경우에 양수값이다. 반대로 스크롤을 올리는 경우에는 음수값이다. 

스크롤을 내리는 경우에는 다음 슬라이드를 보여주기 위하여 인덱스값을 1만큼 증가시킨다. 반대로 스크롤을 내리는 경우에는 이전 슬라이드를 보여주기 위하여 인덱스값을 1만큼 감소시킨다. 물론 현재 슬라이드 갯수는 한정되어 있으므로 인덱스값을 증가하거나 감소할때 인덱스값에 대한 유효성 검증이 필요하다.

마우스 휠 이벤트가 발생하면 현재 웹 화면에 보여주려고 하는 섹션의 투명도를 1로 변경하고 높이는 브라우저 높이로 변경하여 애니메이션이 동작하도록 한다. css 코드에서 섹션의 bottom 속성은 0으로 고정되어 있으므로 섹션의 높이가 증가할때 아래부터 위로 올라가면서 화면이 전환된다. 

// index 에 해당하는 섹션을 제외한 기존스타일 초기화
  // 이전/다음 슬라이드로 모두 전환가능
  for(let i=0; i<sections.length; i++){
      const section = sections[i]
      console.log(i, section)
      section.style.opacity = '0'
      section.style.height = '0'
    
  }

현재는 다음 슬라이드로만 전환되지만, 해당 코드를 주석해제하면 이전/다음 슬라이드로 자유롭게 전환할 수 있다. 이유는 현재 보여주려는 섹션에 애니메이션을 적용하기 전에 모든 슬라이드의 css 스타일을 초기화하기 때문이다. 이렇게 하면 이전 슬라이드에 이미 애니메이션이 적용되었더라도 초기화되기 때문에 다시 애니메이션 효과가 동작한다. 

 

한가지 문제점이 있다. 다음 슬라이드나 이전 슬라이드가 화면에 보이기 시작할때 이전 슬라이드나 다음 슬라이드가 사라지기 시작한다. 그래서 슬라이드가 나타나고, 사라지면서 두 슬라이드가 겹쳐보인다. 이를 해결하기 위하여 setTimeout 메서드를 이용하여 아래와 같이 이전/다음 슬라이드가 화면에 완전히 나타나고 나서(1초 후에), 다음/이전 슬라이드가 화면에서 사라지도록 코드를 수정하였다. 

이렇게 하면 다음 슬라이드가 화면에 완전히 보이고 나서 이전 슬라이드가 사라지므로 자연스러운 화면 전환이 된다. 또한, 이전 슬라이드가 완전히 화면에 나타나고 다음 슬라이드가 사라진다. 실제로 화면에서 보면 다음 슬라이드로 이동할때는 다음 슬라이드가 화면 위쪽으로 상승하는것처럼 보이고, 이전 슬라이드로 이동할때는 현재 슬라이드가 내려오는것처럼 보인다. 

const main = document.querySelector('main')
const sections = main.querySelectorAll('section')
const clientHeight = document.documentElement.clientHeight // 브라우저 높이
const scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
)

let index = 0, timer 

function throttling(handler, e){
    if(!timer){
        timer = setTimeout(function(){
            handler(e)
            timer = null  
        }, 1000)
    }
}
function changeSlide(e){
    console.log('스크롤', e.deltaY)

    if(e.deltaY > 0){ // 스크롤을 내린 경우
        index++
        if(index > sections.length - 1) index = 0
    }else{ // 스크롤을 올린 경우
        index--
        if(index < 0) index = sections.length - 1
    }
    console.log(index)

    for(let i=0; i<sections.length; i++){
        if(index !== i){
            const section = sections[i]
            section.style.transition = 'none'
            
            setTimeout(function(){
                section.style.opacity = '0'
                section.style.height = '0'
                section.style.transition = '1s ease-in-out'
            }, 1000)
        }
    }

    const section = sections[index]
    section.style.opacity = '1'
    section.style.height = '100vh'
}

window.addEventListener('wheel', (e) => throttling(changeSlide, e))

조건문으로 i 값이 현재 인덱스(현재 보여주려는 섹션) 가 아닐때 트랜지션을 제거한다. 그렇게 하지 않고 현재 보여주려는 섹션에도 트랜지션을 제거하면 높이가 증가하지 않고, 곧바로 브라우저 높이로 설정되어 애니메이션이 적용되지 않는다. 

for(let i=0; i<sections.length; i++){
        if(index !== i){
            const section = sections[i]
            section.style.transition = 'none'
            
            setTimeout(function(){
                section.style.opacity = '0'
                section.style.height = '0'
                section.style.transition = '1s ease-in-out'
            }, 1000)
        }
    }

 

* 슬라이드 플립(flip) 애니메이션 효과 구현하기

슬라이드 회전
슬라이드 플립(flip)

<!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>Document</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <section><p>1</p></section>
    <section><p>2</p></section>
    <section><p>3</p></section>
    <section><p>4</p></section>
    <section><p>5</p></section>
  </main>
  <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자! 앞선 예제와 html 파일은 동일하다.

body{
  margin: 0; padding: 0;
  box-sizing: border-box;
}
main{
  width: 100%;
  perspective: 5000px; /* 원근감을 줘서 슬라이드가 플립되도록 함 */
}
main section{
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  position: fixed; left: 50%; right: 0; 
  top: 0; transform: translateX(-50%);
  transition: 1s ease-in-out;
}
main section p{
  font-size: 2rem;
  font-weight: bold;
  color: seashell;
  text-align: center;
  margin: 0; padding: 1rem 2rem;
  /* border: 1px solid red; */
}
main section:nth-child(1){
  background-color: yellowgreen;
}
main section:nth-child(2){
  background-color: brown;
  opacity: 0;
}
main section:nth-child(3){
  background-color: yellow;
  opacity: 0;
}
main section:nth-child(4){
  background-color: springgreen;
  opacity: 0;
}
main section:nth-child(5){
  background-color: slateblue;
  opacity: 0;
}

style.css 파일을 위와 같이 작성하자! 앞선 예제와 css 파일은 유사하다. 다만 원근감과 3차원 효과를 주기 위하여 main 요소에 perspective 속성이 추가되었다. 그리고 Y축 플립(로테이션)을 위하여 섹션의 X축 좌표기준을 섹션 중앙으로 이동하였다. 첫번째 섹션을 제외한 각 섹션의 opacity 만 0으로 설정하여 나머지 슬라이드를 화면에서 숨겼다.

const main = document.querySelector('main')
const sections = main.querySelectorAll('section')
const clientHeight = document.documentElement.clientHeight // 브라우저 높이
const scrollHeight =  Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
)
const speedOfSlide = 0.2
let index = 0, timer, angle = 0

function trotthling(handler, e){   // 100ms 동안 이벤트 금지하기 (자연스러운 슬라이드 효과 연출)
  if(!timer){
    timer = setTimeout(function(){
      handler(e)
      timer = null 
    }, 100)
  }
}
function initializeStyle(sections){
  for(let i=0; i<sections.length; i++){
    const section = sections[i]
    section.style.opacity = '0'
    section.style.transform = 'translateX(-50%)'
    section.style.transition = 'none'
  }
}
function changeSlide(e){
  console.log('scroll', e.deltaY)

  if(e.deltaY > 0){ // 스크롤을 내린 경우
    angle += parseInt(e.deltaY * speedOfSlide) // 스크롤을 내린 거리의 speedOfSlide 비율만큼만 angle 값 증가
    console.log(angle)

    if(angle > 360){ // 다음 슬라이드의 flip 을 위한 angle 초기화 
      angle = 0
      initializeStyle(sections) // angle 이 360도에서 0도로 갑자기 변할때 트랜지션이 적용되어 있어서 슬라이드가 빠르게 회전하는데 이를 방지하고자 트랜지션을 제거함
    }

    if(Math.abs(angle - 90) < 20){ // 90도 +-10도 근처에서 코드블럭 실행
      angle += 180 // 다음 슬라이드의 위상은 270도부터 시작해야 하므로 180도 증가 (슬라이드 flip)
      index++ // 다음 슬라이드 선택을 위한 인덱스값 증가 
      if(index > sections.length - 1){
        index = 0
      }
      setTimeout(function(){ // 부드러운 화면 전환을 위한 타이머 설정후 초기화 적용하기
        initializeStyle(sections) // 이전 슬라이드 감추고 angle 이 급격하게 변했으므로 트랜지션이 적용되어 있을 경우 슬라이드가 빠르게 회전하므로 이를 방지하고자 트랜지션을 제거함
      }, 60)
    }
    console.log(index)

    const section = sections[index]
    section.style.opacity = '1'    // 현재 섹션에 애니메이션 적용
    section.style.transform = `translateX(-50%) rotateY(${parseInt(angle)}deg)` // 현재 섹션을 Y축으로 회전하기

    setTimeout(function(){ 
      section.style.transition = '1s ease-in-out'  // 트랜지션 제거후 회전할때 곧바로 트랜지션을 재설정하면 90도 360도에서 급격히 회전하므로 일정시간 후 재설정함
    }, 50)
  }
}


document.addEventListener('wheel', (e) => trotthling(changeSlide, e))

app.js 파일을 위와 같이 작성한다.

const speedOfSlide = 0.2

스크롤 이벤트가 발생할때 슬라이드가 회전하는데 이때 회전 속도를 조절하기 위한 값이다. 값이 클수록 빠르게 회전한다. 

let angle = 0

슬라이드를 회전시키기 위한 변수를 선언한다.

function trotthling(handler, e){   // 100ms 동안 이벤트 금지하기 (자연스러운 슬라이드 효과 연출)
  if(!timer){
    timer = setTimeout(function(){
      handler(e)
      timer = null 
    }, 100)
  }
}

쓰로틀링 함수는 기존과 동일하지만 쓰로틀 간격을 1초에서 100ms 로 줄였다. 이유는 기존과 다르게 스크롤 이벤트가 발생할때마다 슬라이드가 조금씩 회전할 수 있도록 하기 위함이다. 쓰로틀 간격을 짧게 가져갈수록 부드럽게 회전하고, 길게 가져갈수록 회전이 느리거나 부자연스럽게 보인다.

function initializeStyle(sections){  // 섹션의 기존스타일 초기화 (다른 슬라이드 숨기기)
  for(let i=0; i<sections.length; i++){
    const section = sections[i]
    section.style.opacity = '0'
    section.style.transform = 'translateX(-50%)'
    section.style.transition = 'none'
  }
}

현재 화면에 보여줄 슬라이드를 제외한 나머지 슬라이드는 모두 화면에서 숨기고, 다음번 애니메이션 적용을 위하여 초기화한다. 그렇지 않으면 애니메이션이 한번만 적용되고, 다음번에는 애니메이션이 발생하지 않는다. 

function changeSlide(e){
  console.log('scroll', e.deltaY)

  if(e.deltaY > 0){ // 스크롤을 내린 경우
    angle += parseInt(e.deltaY * speedOfSlide) // 스크롤을 내린 거리의 speedOfSlide 비율만큼만 angle 값 증가
    console.log(angle)

    if(angle > 360){ // 다음 슬라이드의 flip 을 위한 angle 초기화 
      angle = 0
      initializeStyle(sections) // angle 이 360도에서 0도로 갑자기 변할때 트랜지션이 적용되어 있어서 슬라이드가 빠르게 회전하는데 이를 방지하고자 트랜지션을 제거함
    }

    if(Math.abs(angle - 90) < 20){ // 90도 +-10도 근처에서 코드블럭 실행
      angle += 180 // 다음 슬라이드의 위상은 270도부터 시작해야 하므로 180도 증가 (슬라이드 flip)
      index++ // 다음 슬라이드 선택을 위한 인덱스값 증가 
      if(index > sections.length - 1){
        index = 0
      }
      setTimeout(function(){ // 부드러운 화면 전환을 위한 타이머 설정후 초기화 적용하기
        initializeStyle(sections) // 이전 슬라이드 감추고 angle 이 급격하게 변했으므로 트랜지션이 적용되어 있을 경우 슬라이드가 빠르게 회전하므로 이를 방지하고자 트랜지션을 제거함
      }, 60)
    }
    console.log(index)

    const section = sections[index]
    section.style.opacity = '1'    // 현재 섹션에 애니메이션 적용
    section.style.transform = `translateX(-50%) rotateY(${parseInt(angle)}deg)` // 현재 섹션을 Y축으로 회전하기

    setTimeout(function(){  // 슬라이드 flip 후 자연스러운 슬라이드 움직임을 위한 트랜지션 재설정
      section.style.transition = '1s ease-in-out'  // 트랜지션 제거후 회전할때 곧바로 트랜지션을 재설정하면 90도 360도에서 급격히 회전하므로 일정시간 후 재설정함
    }, 50)
  }
}

스크롤 이벤트가 발생할때 실행되는 이벤트핸들러 함수이다. 

if(e.deltaY > 0){ // 스크롤을 내린 경우
 // 코드블럭
 }

스크롤을 내린 경우만 다음 슬라이드로 전환할 수 있도록 하였다. 역방향은 동작하지 않는다. 

angle += parseInt(e.deltaY * speedOfSlide) // 스크롤을 내린 거리의 speedOfSlide 비율만큼만 angle 값 증가

스크롤을 내린 거리를 회전각도로 변환한다. 스크롤을 내린 거리를 그대로 회전각도로 사용하지 않고, speedOfSlide 값의 비율만큼만 사용한다. 현재 speedOfSlide 값은 0.2이므로 스크롤로 이동한 거리의 20%만큼만 회전하도록 한다.

angle += 1

스크롤을 내린 거리를 히전각도로 변환하지 않고, 그냥 스크롤할때마다 1도씩 회전시켜도 동작한다.

if(Math.abs(angle - 90) < 20){ // 90도 +-10도 근처에서 코드블럭 실행
  angle += 180 // 다음 슬라이드의 위상은 270도부터 시작해야 하므로 180도 증가 (슬라이드 flip)
  index++ // 다음 슬라이드 선택을 위한 인덱스값 증가 
  if(index > sections.length - 1){
    index = 0
  }
  setTimeout(function(){ // 부드러운 화면 전환을 위한 타이머 설정후 초기화 적용하기
    initializeStyle(sections) // 이전 슬라이드 감추고 angle 이 급격하게 변했으므로 트랜지션이 적용되어 있을 경우 슬라이드가 빠르게 회전하므로 이를 방지하고자 트랜지션을 제거함
  }, 60)
}

회전각도(angle)가 90도에 가까워지면 다음 슬라이드로의 화면전환을 위해 인덱스값을 증가시킨다. 또한, 다음 슬라이드의 위상은 90도가 아니라 플립(180도 회전)해서 270도에서 회전해야 하므로 180도만큼 회전각도를 더해준다. 타이머를 설정해서 부드럽게 화면이 전환될 수 있도록 한다. 타이머를 설정하지 않으면 갑자기 다음 슬라이드로 플립(180도 회전)하므로 화면전환이 부드럽지 않다. 이유는 initializeStyle 함수에서 트랜지션을 제거하기 때문이다. 

const section = sections[index]
section.style.opacity = '1'    // 현재 섹션에 애니메이션 적용
section.style.transform = `translateX(-50%) rotateY(${parseInt(angle)}deg)` // 현재 섹션을 Y축으로 회전하기

setTimeout(function(){  // 슬라이드 flip 후 자연스러운 슬라이드 움직임을 위한 트랜지션 재설정
  section.style.transition = '1s ease-in-out'  // 트랜지션 제거후 회전할때 곧바로 트랜지션을 재설정하면 90도 360도에서 급격히 회전하므로 일정시간 후 재설정함
}, 50)

스크롤 이벤트가 발생하면 현재 인덱스의 슬라이드를 화면에 보여준다. 즉, opacity 값을 1로 설정한다. 또한, 스크롤 내린 거리만큼 슬라이드를 회전한다. 즉, rotateY 함수를 사용한다.

이때 회전각도(angle)가 90도 근처이면 플립(flip)을 위해 각도를 180도로 급격히 증가시킨다. 트랜지션이 설정되어 있으면 슬라이드가 급격히 회전하므로 이를 방지하기 위하여 트랜지션을 제거한다. 다음 슬라이드로 전환된 이후에는 회전각도가 급격히 변하지 않으므로 부드러운 회전을 위하여 트랜지션을 재설정해줘야 한다. 

물론 회전각도가 360도에서 0도로 초기화되는 경우에도 슬라이드가 갑자기 빠르게 회전하는데 이를 막기 위하여 트랜지션을 제거한다. 이후 슬라이드의 회전각도가 0도보다 커지면 회전각도는 급격히 변하지 않고 자연스럽게 증가하므로 다시 트랜지션을 재설정하여 슬라이드가 자연스럽게 회전할 수 있도록 한다. 

if(angle > 360){ // 다음 슬라이드의 flip 을 위한 angle 초기화 
  angle = 0
  initializeStyle(sections) // angle 이 360도에서 0도로 갑자기 변할때 트랜지션이 적용되어 있어서 슬라이드가 빠르게 돌면서 로테이션되는데 이를 방지하고자 트랜지션을 제거함
}

다음 슬라이드로 화면전환을 하기 위해서는 360도가 넘으면 다시 0으로 초기화해줘야 한다. 이때 회전각도가 360도에서 0도로 초기화되면서 rotateY 함수에 의해 슬라이드가 갑자기 빠르게 회전하는데 이를 막기 위한 방법으로 트랜지션을 제거하여 사람이 눈치채기 전에 빠르게 0도 지점으로 슬라이드를 이동시킨다. 

 

* 매트릭스 텍스트 애니메이션 구현하기 

매트릭스 텍스트 애니메이션 구현하기

<!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>Document</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <pre>      인생은 롤러코스터와 같아.

      둘다 오르막과 내리막이 있으니깐
      
      하지만 두려움에 떨거나 즐기는 것은
      
      너의 선택이야
      </pre>
    <script src="app.js"></script>
  </body>
  </html>

index.html 파일을 위와 같이 작성하자!

body{
  margin: 0; padding: 0;
  background-color: #0e1111;
  display: flex;
  justify-content: center;
  align-items: center;
}
body pre{
  color: green;
  font-size: 1.2rem;
}
.letter{
  position: absolute;
  top: 0;
  color: green;
  transition: 20ms ease-in-out;
}

style.css 파일을 위와 같이 작성하자!

const paragraph = document.querySelector('pre')
const text = paragraph.innerText
const scrollbarWidth = 50
const maxFontSize = 30
const widthOfBrowser = document.documentElement.clientWidth - (scrollbarWidth + maxFontSize) // 브라우저 높이 - (스크롤바 너비 + 글자너비)
let isStarted = false // 스크롤시 애니메이션을 한번만 실행하는 플래그 변수 

function pickRandomNumber(n){
  return Math.floor(Math.random() * n)
}

function pickRandomLetter(text){ // 문자열에서 랜덤글자를 선택하는 함수
  const randomIndex = pickRandomNumber(text.length)
  return text[randomIndex]
}
function createLetter(text, left, top){ // 문자열, 위치(x, y) 좌표를 입력으로 받아서 글자를 생성하는 함수
  console.log('creating letter...')
  const span = document.createElement('span')
  span.className = 'letter'
  span.style.left = left + 'px'
  span.style.fontSize = pickRandomNumber(maxFontSize) + 'px' // 최대 폰트크기보다 작은 글자 생성
  span.innerText = pickRandomLetter(text) 
  if(top) span.style.top = top + 'px' 
  return span 
}

function startTextAnimation(){
  if(!isStarted){
    paragraph.innerText = '' // 브라우저 화면 초기화 
    setInterval(function(){ // 랜덤글자를 생성하고 화면에 보여주기 위한 타이머 설정
      const letter = createLetter(text, pickRandomNumber(widthOfBrowser)) // 랜덤한 위치에 랜덤한 글자 생성 (브라우저 너비를 벗어나지 않는 범위에서)
      document.body.appendChild(letter) // 화면에 랜덤글자 디스플레이 
      setInterval(function(){ // 생성한 랜덤글자를 Y축으로 이동시키기 위한 타이머 설정 
        letter.style.top = letter.offsetTop + 3 + 'px' // 생성한 랜덤글자를 아래방향으로 이동
        const cloneLetter = createLetter(text, letter.offsetLeft, letter.offsetTop) // 처음에 생성한 랜덤글자와 동일한 글자를 현재 Y측 위치에 복제
        document.body.appendChild(cloneLetter) // 복제된 랜덤글자를 화면에 디스플레이 
      }, 30)
    }, 100)

    isStarted = true 
  }
}

window.addEventListener('wheel', startTextAnimation)

app.js 파일을 위와 같이 작성하자!

하지마 한가지 문제점은 브라우저 높이의 중반 이후부터는 조금씩 흘러내리는 속도가 느려진다는 점이다. 그래서 아래와 같이 글자 생성시 setInterval 메서드를 setTimeout 으로 변경해주면 조금더 늦게 속도가 느려진다. 

const paragraph = document.querySelector('pre')
const text = paragraph.innerText
const scrollbarWidth = 50
const maxFontSize = 30
const widthOfBrowser = document.documentElement.clientWidth - (scrollbarWidth + maxFontSize) // 브라우저 높이 - (스크롤바 너비 + 글자너비)
let isStarted = false // 스크롤시 애니메이션을 한번만 실행하는 플래그 변수 
let timer

function pickRandomNumber(n){
  return Math.floor(Math.random() * n)
}

function pickRandomLetter(text){ // 문자열에서 랜덤글자를 선택하는 함수
  const randomIndex = pickRandomNumber(text.length)
  return text[randomIndex]
}
function createLetter(text, left, top){ // 문자열, 위치(x, y) 좌표를 입력으로 받아서 글자를 생성하는 함수
  console.log('creating letter...')
  const span = document.createElement('span')
  span.className = 'letter'
  span.style.left = left + 'px'
  span.style.fontSize = pickRandomNumber(maxFontSize) + 'px' // 최대 폰트크기보다 작은 글자 생성
  span.innerText = pickRandomLetter(text) 
  if(top) span.style.top = top + 'px' 
  return span 
}

function displayLetter(){ // 랜덤글자를 생성하고 화면에 보여주기 위한 타이머 설정
  if(timer) clearTimeout(timer)

  // const randomLetter = pickRandomLetter(text) 
  const letter = createLetter(text , pickRandomNumber(widthOfBrowser)) // 랜덤한 위치에 랜덤한 글자 생성 (브라우저 너비를 벗어나지 않는 범위에서)
  document.body.appendChild(letter) // 화면에 랜덤글자 디스플레이 
  setInterval(function(){ // 생성한 랜덤글자를 Y축으로 이동시키기 위한 타이머 설정 
    letter.style.top = letter.offsetTop + 7 + 'px' // 생성한 랜덤글자를 아래방향으로 이동
    const cloneLetter = createLetter(text, letter.offsetLeft, letter.offsetTop) // 처음에 생성한 랜덤글자와 동일한 글자를 현재 Y측 위치에 복제
    document.body.appendChild(cloneLetter) // 복제된 랜덤글자를 화면에 디스플레이 
  }, 30)
  timer = setTimeout(displayLetter, 100)
}

function startTextAnimation(){
  if(!isStarted){
    paragraph.innerText = '' // 브라우저 화면 초기화 
    setTimeout(displayLetter, 100)

    isStarted = true 
  }
}

window.addEventListener('wheel', startTextAnimation)

 

 

* 키 이벤트로 간단한 캐릭터 움직임 만들기

<!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'></div>
    <div id='bar'></div>
    <script src="app.js"></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자! box 요소는 화면에 캐릭터를 보여준다. bar 요소는 화면에 그라운드(땅)을 보여준다.

#box{
    width: 50px;
    height: 50px;
    background-color: saddlebrown;
    position: absolute;
    left: 0px;
    top: -100px;
    transition: all 0.1s linear;
    background-image: url('super-mario-right.jpg');
    background-size: cover;
    /* 커서 깜빡임 해결 */
    caret-color: transparent; 
}
#bar{
    width: 700px;
    height: 50px;
    background-color: peru;
    position: absolute;
    left: 0;
    top: 550px;
}

style.css 파일을 위와 같이 작성하자! box 요소는 캐릭터를 스타일링한다. bar 요소는 그라운드(땅)을 스타일링한다.

const box = document.getElementById('box') // 캐릭터
const gravity = 1 // 중력가속도
const FPS = 60 // 프레임수
const limitBottom = 500 // 그라운드(땅) 위치
const limitLeft = 700 // 낭떠러지 위치
const jumpHeight = 30 // 점프 높이


let vx = 20 // x 방향 속도
let vy = 0 // y 방향 속도
let isJumping = false // 점프가능 여부
let isDead = false // 캐릭터 죽음 여부
let jumpKey = false // 점프키 활성화 여부

// 점프하다가 vy 가 jumpHeight 보다 커지면 jumpKey 가 false 가 되면서 더이상 아래로 떨어지지 않고 그라운드(땅)에 정지함
// 중력은 계속 작용하니까 중력에 의해서 아래로 내려오다가 limitBottom 에 닿으면 isJumping 이 true 가 되면서 점핑이 가능하게 됨
// 슈퍼 마리오가 limitLeft 값을 넘어가면, 즉, 땅을 벗어나면 죽었으므로 isDead 가 true 가 되고 isDead 가 true 이면 계속 아래로 떨어짐

// 점프 높이에 영향을 주는 요소 : transition duration, jumpHeight, FPS

function down(){
    // 캐릭터 y 방향 위치 조회
    const topStyle = window.getComputedStyle(box).top
    let top = parseInt(topStyle)


    // 캐릭터 아래로 내려오게 하기 (중력가속도에 의한 가속도 운동)
    vy += gravity
    top += vy

    // 점프했다가 그라운드(땅)에 다시 내려오는 순간 그라운드(땅)에 정지시키기
    if(vy >= jumpHeight){
        jumpKey = false
    }
    
    // 게임 종료
    if(isDead && top > limitBottom + 700){
        alert('Game over !')
        clearInterval(timerId)
    }

    console.log('속도', vy)

    // 그라운드(땅)에 정지시키기
    if(!isDead && !jumpKey && top >= limitBottom){
        top = limitBottom // 캐릭터 위치 고정하기
        isJumping = true // 점프 활성화
        vy = 0 // y 방향 속도 초기화
    }

    // 화면에서 캐릭터 y 방향 위치 변경
    box.style.top = `${top.toString()}px`
}

// 지속적인 애니메이션 실행
const timerId = setInterval(down ,1000/FPS)


// 캐릭터 이동시키기
function move(e){
   
    // 캐릭터 x 방향 위치 조회
    const leftStyle = window.getComputedStyle(box).left
    let left = parseInt(leftStyle)

    // 캐릭터 y 방향 위치 조회
    const topStyle = window.getComputedStyle(box).top
    let top = parseInt(topStyle)

    // 화살표 우측키를 누른 경우
    if(e.keyCode === 39){
        box.style.backgroundImage = "url('super-mario-right.jpg')"; // 캐릭터 이미지 변경
        left += vx // 캐릭터 우측으로 이동
        if(left > limitLeft){ // 그라운드(땅)을 벗어난 경우
            isDead = true // 캐릭터 사망
        }
    }
    // 화살표 좌측키를 누른 경우
    else if(e.keyCode === 37){
        box.style.backgroundImage = "url('super-mario-left.jpg')"; // 캐릭터 이미지 변경
        if(left > 0){ // 윈도우 화면의 좌측 경계를 벗어나지 않은 경우
            left -= vx // 캐릭터 좌측으로 이동
        }
    }
    // 스페이스바나 화살표 위쪽키를 누른 경우
    else if(e.keyCode === 32 || e.keyCode === 38){
        if(isJumping){ // 그라운드(땅) 위에 있는 경우
            vy = -jumpHeight // 점프 가능하도록 y 방향 속도의 방향을 변경함
            isJumping = false // 점프 비활성화
            jumpKey = true // 그라운드(땅)에서 점프함
        }
    }

    box.style.left = `${left.toString()}px` // 캐릭터 x 방향 위치 변경
    box.style.top = `${top.toString()}px` // 캐릭터 y 방향 위치 변경
}
window.addEventListener('keydown', move)

app.js 파일을 위와 같이 작성하자!

const box = document.getElementById('box')

box 요소의 스타일을 변경하기 위하여 검색한다.

const gravity = 1

캐릭터에게 물리적인 중력 가속도를 적용하기 위하여 gravity 변수를 선언한다.

const FPS = 60 // 프레임수

FPS 는 Frames Per Second 의 약자로 1초에 화면을 몇번 새로 그릴지를 결정한다. 현재는 30으로 설정되어 있으므로 1초에 화면을 30번 교체한다. 

const limitBottom = 500 // 그라운드(땅) 위치

그라운드(땅)의 위아래 위치를 의미한다. 현재는 500px 에 위치하도록 설정한다. bar 요소는 그라운드를 의미하고, 스타일 코드에서 bar 요소의 top 이 550px 이기 때문에 limitBottom 을 500px 로 설정하였다. 50px 이 다른 것은 box (캐릭터)의 높이가 50px 이기 때문이다. 

const limitLeft = 700 // 낭떠러지 위치

그라운드(땅)의 너비를 의미한다. 현재는 700px 로 설정한다. bar 요소의 스타일 코드를 보면 width 가 700px 로 설정되어 있다. 

const jumpHeight = 30 // 점프 높이

jumpHeight 은 캐릭터 점프 높이에 제한을 두기 위함이다. 캐릭터가 그라운드에 가까워지면 다시 점프할 수 있도록 하였다.

let vx = 20 // x 방향 속도

캐릭터의 x 방향 속도를 의미한다. 현재 코드에서는 100px 만큼 이동한다. 

let vy = 0 // y 방향 속도

캐릭터의 y 방향 속도를 의미한다. 현재 코드에서는 0px 로 설정되어 있다. 추후에 중력 가속도가 적용되어 y 방향 속도가 증가하게 된다. 

let isJumping = false // 점프가능 여부

캐릭터의 점프를 허용할지 여부를 결정하는 변수이다. 

let isDead = false // 캐릭터 죽음 여부

캐릭터의 죽음 여부를 판단하기 위한 변수이다. 

let jumpKey = false // 점프키 활성화 여부

캐릭터 점프를 허용할지 말지 결정하는 변수이다. 

const timerId = setInterval(down ,1000/FPS)

윈도우 객체의 setInterval 메서드를 사용하여 프레임을 변경한다. 1000/FPS 는 1초에 60번 프레임을 변경하기 위하여 몇 초마다 한번씩 down 함수를 실행할지 설정한다. 

function down(){
    // 캐릭터 y 방향 위치 조회
    const topStyle = window.getComputedStyle(box).top
    let top = parseInt(topStyle)


    // 캐릭터 아래로 내려오게 하기 (중력가속도에 의한 가속도 운동)
    vy += gravity
    top += vy

    // 점프했다가 그라운드(땅)에 다시 내려오는 순간 그라운드(땅)에 정지시키기
    if(vy >= jumpHeight){
        jumpKey = false
    }
    
    // 게임 종료
    if(isDead && top > limitBottom + 700){
        alert('Game over !')
        clearInterval(timerId)
    }

    console.log('속도', vy)

    // 그라운드(땅)에 정지시키기
    if(!isDead && !jumpKey && top >= limitBottom){
        top = limitBottom // 캐릭터 위치 고정하기
        isJumping = true // 점프 활성화
        vy = 0 // y 방향 속도 초기화
    }

    // 화면에서 캐릭터 y 방향 위치 변경
    box.style.top = `${top.toString()}px`
}

하나의 프레임 안에서 실행되는 코드이다. 프로그램이 실행되는 동안 지속적으로 캐릭터에 중력을 적용하기 위함이다. 

// 캐릭터 y 방향 위치 조회
const topStyle = window.getComputedStyle(box).top
let top = parseInt(topStyle)

캐릭터의 현재 y 축 방향 위치를 조회하는 코드이다. getComputedStyle 메서드는 css 파일에 설정된 DOM 객체의 속성 값을 조회한다. topStyle 은 문자열 형태이므로 parseInt 함수를 이용하여 숫자로 변경한다. 

// 캐릭터 아래로 내려오게 하기 (중력가속도에 의한 가속도 운동)
vy += gravity
top += vy

캐릭터의 y 방향 속도와 위치를 변경하는 코드이다. vy += gravity 는 속도를 변경한다. top += vy 는 위치를 변경한다. 이는 현실에서처럼 캐릭터에게 중력 가속도를 적용한다.

// 슈퍼마리오가 땅에 있는 동안에는 죽지 않았으므로 더이상 아래로 떨어지지 않도록 하기
if(!isDead && !jumpKey && top >= limitBottom){
    top = limitBottom // 캐릭터 위치 고정하기
    isJumping = true // 점프 활성화
    vy = 0 // y 방향 속도 초기화
}

isDead 는 캐릭터가 그라운드(땅)를 벗어나면 true 가 된다. isDead 가 true 이면 캐릭터가 그라운드(땅)보다 아래로 떨어져서 게임이 끝나야 하므로 캐릭터의 y 축 위치가 계속 증가하면 된다. 즉, isDead 가 true 이면 해당코드가 실행되지 않도록 한다. 반대로 캐릭터가 그라운드(땅)를 벗어나지 않은 상태라면 isDead 는 false 이므로 캐릭터가 살아있는 경우에만 해당 코드를 실행하여 그라운드(땅)에 캐릭터를 안착시킨다. 

스페이스바나 화살표 위쪽 방향키를 누르면 jumpKey 가 true 로 변경되어 해당 조건문 안으로 들어오지 않는다. 즉, 캐릭터의 y 방향 위치가 그라운드(땅)에 붙어있지 않고, 점프한 거리만큼 이동한다. 다시말해, 점프가 가능해진다. isJumping 변수는 단순히 스페이스바나 화살표키를 눌렀을때 캐릭터가 그라운드(땅)에 붙어있는지 검사한다. 그라운드(땅)에 붙어있으면 jumpKey를 true 로 변경하여 실제로 캐릭터가 그라운드(땅)에서 떨어지도록 한다. isJumping 변수가 false 이면 캐릭터가 아직 그라운드(땅)위에 떠있는 상태이므로 스페이스바나 화살표키를 눌러도 점프가 되지 않는다. 즉, 그라운드(땅)에 떠있을때는 점프가 되지 않도록 하여 중력에 의해 캐릭터가 다시 아래로 내려올수 있도록 한다.

현재 캐릭터의 y 방향 위치가 limitBottom 을 넘으면 y 방향 위치를 limitBottom 으로 설정하여 캐릭터를 그라운드(땅)에 고정시킨다. 다시말해, 이렇게 되면 캐릭터가 중력 가속도에 의하여 y 방향 위치가 증가하다가도 limitBottom 을 넘어가는 순간 더이상 아래로 떨어지지 않고 멈춘다. 캐릭터가 땅에 닿으면 멈추기 때문에 y방향 속도는 0으로 초기화한다. 또한, 캐릭터가 그라운드(땅)에 닿으면 isJumping 을 true 로 설정하여 그라운드(땅)에 닿았을때만 점프가 가능하게 한다. 

box.style.top = `${top.toString()}px`

중력 가속도에 의하여 변경된 캐릭터의 y 축 방향 위치값으로 캐릭터의 css 스타일을 업데이트한다.  즉, 캐릭터를 y축 방향으로 이동시킨다. 

// 점프했다가 그라운드(땅)에 다시 내려오는 순간 그라운드(땅)에 정지시키기
if(vy >= jumpHeight){
    jumpKey = false
}

포물선 그래프에 의하여 vy 값이 jumpHeight 보다 커지면 다시 그라운드(땅)에 도달한 시점으로 이때 jumpKey 를 false 로 설정하여 그라운드(땅)에서 캐릭터가 멈추도록 한다. 

// 게임 종료
if(isDead && top > limitBottom + 700){
    alert('Game over !')
    clearInterval(timerId)
}

isDead 가 true 라는 말은 캐릭터가 그라운드(땅)을 벗어난 상태이다. 이때 캐릭터가 그라운드(땅)보다 700px 더 아래로 떨어지면 게임종료를 화면에 띄우고 타이머를 종료한다. 또한, isDead 가 true 이면 아래 조건문이 실행되지 않으므로 캐릭터가 그라운드(땅)에 고정되어 있지 않고 중력에 의해 계속 아래로 떨어진다. 

// 그라운드(땅)에 정지시키기
    if(!isDead && !jumpKey && top >= limitBottom){
        top = limitBottom // 캐릭터 위치 고정하기
        isJumping = true // 점프 활성화
        vy = 0 // y 방향 속도 초기화
    }

 

캐릭터에 키 이벤트를 설정하여 캐릭터를 이동하도록 한다. 

window.addEventListener('keydown', move)

키 이벤트로 캐릭터에 움직임을 적용하기 위하여 window 객체에 keydown 이벤트를 등록한다. keydown 이벤트가 발생하면 move 이벤트핸들러 함수가 실행된다. 

// 캐릭터 이동시키기
function move(e){
   
    // 캐릭터 x 방향 위치 조회
    const leftStyle = window.getComputedStyle(box).left
    let left = parseInt(leftStyle)

    // 캐릭터 y 방향 위치 조회
    const topStyle = window.getComputedStyle(box).top
    let top = parseInt(topStyle)

    // 화살표 우측키를 누른 경우
    if(e.keyCode === 39){
        box.style.backgroundImage = "url('super-mario-right.jpg')"; // 캐릭터 이미지 변경
        left += vx // 캐릭터 우측으로 이동
        if(left > limitLeft){ // 그라운드(땅)을 벗어난 경우
            isDead = true // 캐릭터 사망
        }
    }
    // 화살표 좌측키를 누른 경우
    else if(e.keyCode === 37){
        box.style.backgroundImage = "url('super-mario-left.jpg')"; // 캐릭터 이미지 변경
        if(left > 0){ // 윈도우 화면의 좌측 경계를 벗어나지 않은 경우
            left -= vx // 캐릭터 좌측으로 이동
        }
    }
    // 스페이스바나 화살표 위쪽키를 누른 경우
    else if(e.keyCode === 32 || e.keyCode === 38){
        if(isJumping){ // 그라운드(땅) 위에 있는 경우
            vy = -jumpHeight // 점프 가능하도록 y 방향 속도의 방향을 변경함
            isJumping = false // 점프 비활성화
            jumpKey = true // 그라운드(땅)에서 점프함
        }
    }

    box.style.left = `${left.toString()}px` // 캐릭터 x 방향 위치 변경
    box.style.top = `${top.toString()}px` // 캐릭터 y 방향 위치 변경
}

캐릭터의 x, y 방향 이동과 점프를 위한 코드이다. 

 // 캐릭터 x 방향 위치 조회
const leftStyle = window.getComputedStyle(box).left
let left = parseInt(leftStyle)

캐릭터의 x 방향 위치를 알아내기 위하여 getComputedStyle 메서드를 이용하여 css 파일에서 box 의 left 속성값을 조회한다. 

// 캐릭터 y 방향 위치 조회
const topStyle = window.getComputedStyle(box).top
let top = parseInt(topStyle)

캐릭터의 y 방향 위치를 알아내기 위하여 getComputedStyle 메서드를 이용하여 css 파일에서 box 의 top 속성값을 조회한다. 

let left = parseInt(leftStyle)
let top = parseInt(topStyle)

css 파일의 속성값은 문자열이므로 계산을 위하여 숫자 형태로 변형한다. 

// 화살표 우측키를 누른 경우
if(e.keyCode === 39){
    box.style.backgroundImage = "url('super-mario-right.jpg')"; // 캐릭터 이미지 변경
    left += vx // 캐릭터 우측으로 이동
    if(left > limitLeft){ // 그라운드(땅)을 벗어난 경우
        isDead = true // 캐릭터 사망
    }
}

우측 화살표키 (->) 를 누를때마다 실행된다. 캐릭터를 x 방향에 대하여 오른쪽으로 이동시킨다. 

box.style.backgroundImage = "url('super-mario-right.jpg')";

캐릭터 이미지를 변경한다. 캐릭터가 오른쪽으로 이동하는 것처럼 보이게 box 의 배경화면을 변경한다. 

left += vx

 캐릭터의 x 방향 위치를 변경한다. 우측 화살표 키를 누를때마다 캐릭터를 x 방향으로 vx (20px)만큼 이동시킨다.  

if(left > limitLeft){
    isDead = true
}

캐릭터가 오른쪽으로 이동하다가 그라운드(땅)을 벗어나면 죽음을 의미하므로 isDead 를 true 로 변경한다. 

// 화살표 좌측키를 누른 경우
else if(e.keyCode === 37){
    box.style.backgroundImage = "url('super-mario-left.jpg')"; // 캐릭터 이미지 변경
    if(left > 0){ // 윈도우 화면의 좌측 경계를 벗어나지 않은 경우
        left -= vx // 캐릭터 좌측으로 이동
    }
}

좌측 화살표키 (<-) 를 누를때 실행된다. 캐릭터를 x 방향에 대하여 왼쪽으로 이동시킨다. 

box.style.backgroundImage = "url('super-mario-left.jpg')";

캐릭터 이미지를 변경한다. 캐릭터가 왼쪽으로 이동하는 것처럼 보이게 box 의 배경화면을 변경한다. 

if(left > 0){
    left -= vx
}

캐릭터가 왼쪽으로 이동하다가 화면을 벗어날 수 있으므로 캐릭터의 x 방향 위치가 0 보다 큰 경우에만 왼쪽으로 이동시킨다. 만약 캐릭터의 x 방향 위치가 0 보다 작아져서 화면의 좌측 경계를 벗어나면 더이상 왼쪽으로 이동하지 않고 멈추도록 한다. 

// 스페이스바나 화살표 위쪽키를 누른 경우
else if(e.keyCode === 32 || e.keyCode === 38){
    if(isJumping){ // 그라운드(땅) 위에 있는 경우
        vy = -jumpHeight // 점프 가능하도록 y 방향 속도의 방향을 변경함
        isJumping = false // 점프 비활성화
        jumpKey = true // 그라운드(땅)에서 점프함
    }
}

스페이스바(space bar)나 위쪽 화살표키를 누를때 실행된다. 캐릭터가 점프할 수 있도록 하는 코드이다. 

if(isJumping){ // 그라운드(땅) 위에 있는 경우
    vy = -jumpHeight // 점프 가능하도록 y 방향 속도의 방향을 변경함
    isJumping = false // 점프 비활성화
    jumpKey = true // 그라운드(땅)에서 점프함
}

isJumping 이 true 이면 캐릭터가 그라운드(땅)에 멈춰있는 상태이다. 캐릭터가 그라운드(땅)에 멈춰있을때 점프가 가능하도록 jumpKey 를 true 로 설정한다. jumpKey 를 true 로 설정하면 아래 조건문이 실행되지 않으므로 캐릭터가 그라운드(땅)에 멈춰있지 않고 이동이 가능해진다. 즉, 위치변경이 가능해진다. 캐릭터의 y 방향 속도(vy)를 -jumpHeight 으로 설정하여 브라우저 위쪽 방향으로 점프할 수 있도록 한다. vy 가 음수값이면 캐릭터의 y 방향 위치가 위로 상승한다. 단, jumpHeight (30) 이 중력가속도(1)보다 커야 점프한다. 

// 그라운드(땅)에 정지시키기
if(!isDead && !jumpKey && top >= limitBottom){
    top = limitBottom // 캐릭터 위치 고정하기
    isJumping = true // 점프 활성화
    vy = 0 // y 방향 속도 초기화
}

isJumping 을 true 로 유지하면 스페이스바나 화살표 위쪽 방향키를 눌렀을때 아래 조건문이 다시 실행된다. 이렇게 되면 점프한 상태 (캐릭터가 공중에 떠있는 상태)에서 다시 점프가 가능해진다. 이러면 스페이스바를 여러번 누를때 캐릭터는 브라우저 상단보다 위쪽으로 벗어나버린다. 그렇기 때문에 현재 캐릭터가 점프상태인 경우(공중에 떠있는 상태)에는 스페이스바를 누르더라도 더이상 점프가 추가적으로 되지 않도록 isJumping 을 false 로 변경해줘야 한다. 이렇게 하면 한번 점프한 상태에서 중력가속도에서 의하여 포물선을 그리면서 캐릭터가 다시 아래로 내려온다. 

if(isJumping){ // 그라운드(땅) 위에 있는 경우
    vy = -jumpHeight // 점프 가능하도록 y 방향 속도의 방향을 변경함
    isJumping = false // 점프 비활성화
    jumpKey = true // 그라운드(땅)에서 점프함
}

 

box.style.left = `${left.toString()}px` 
box.style.top = `${top.toString()}px`

변경된 캐릭터의 x, y 방향 위치를 css 스타일에 적용하여 화면에서 캐릭터를 이동시킨다. 

 

* 브라우저 데이터 저장 (localStorage, sessionStorage)

기본적으로 웹서비스는 데이터를 서버에 저장하지만, 웹서비스의 성능향상과 편의를 위하여 브라우저에 임시적으로 데이터를 저장해야 할 경우가 있다. 이때 사용하는 브라우저 API 가 localStorage 메서드와 sessionStorage 메서드이다. 

기본적인 프로그램 변수가 메모리에 저장되지만 해당 메서드에 의하여 정의된 변수는 브라우저에 저장되어 브라우저를 새로고침하더라도 여전히 데이터가 남아있다. 

 

localStorage

로컬 스토리지는 브라우저를 닫았다가 다시 열어도 설정한 값이 유지가 된다. 또한, 사용자가 삭제할때까지 유지된다. 그러므로 사용자가 데이터를 지우지 않는 이상 해당 웹서비스에 localStorage 로 저장한 데이터는 영구적으로 저장된다. 향후 다시 해당 웹서비스 방문시 저장된 데이터를 이용 가능하다. 

localStorage.setItem('name', 'syleemomo') // 로컬스토리지 객체에 name 프로퍼티를 syleemomo 로 설정함
localStorage.getItem('name') // 로컬스토리지 객체의 name 프로퍼티를 조회함
localStorage.removeItem('name') // 로컬스토리지 객체의 name 프로퍼티를 삭제함
localStorage.clear() // 로컬스토리지 객체 자체를 제거함

localStorage 객체에는 위와 같은 메서드들이 존재한다. 각각의 역학을 주석을 참고하면 된다. 

 

 

sessionStorage

세션 스토리지는 브라우저를 닫으면 설정한 값이 제거된다. 또한, 사용자가 브라우저를 새로고침하더라도 데이터가 남아 있어서 조회가 가능하다. 하지만 localStorage 처럼 영구적으로 데이터가 보존되지는 않는다. 

sessionStorage.setItem('name', 'syleemomo') // 세션스토리지 객체에 name 프로퍼티를 syleemomo 로 설정함
sessionStorage.getItem('name') // 세션스토리지 객체의 name 프로퍼티를 조회함
sessionStorage.removeItem('name') // 세션스토리지 객체의 name 프로퍼티를 삭제함
sessionStorage.clear() // 세션스토리지 객체 자체를 제거함

 

사용자가 업로드한 이미지 파일을 로컬 스토리지에 저장하여 새로고침 하더라도 업로드한 파일이 보이게 해보자!

<!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>
    <input id="file-input" type="file">
    <div id='img-box'></div>

    <script src='app.js'></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

body{
    margin: 0;
    padding: 0;
}
#file-input{
    position: absolute;
    left: 50%;
    top: 100px;
    transform: translate(-50%);
}
#img-box{
    width: 200px;
    height: 250px;
    overflow: hidden;

    position: absolute;
    left: 50%;
    top: 200px;
    transform: translate(-50%);
}
#img-box img{
    width: 100%;
    height: 100%;
}

style.css 파일을 위와 같이 작성하자!

const fileInput = document.getElementById('file-input')
const imgBox = document.getElementById('img-box')

function isValid(type){
    return type.split('/')[0] === 'image'
}

function displayImg(src){
    imgBox.innerHTML = `<img src='${src}'/>`
}

function rememberImg(e){
    console.log(e.target.result) // reader 객체로 읽어온 데이터  (이미지 경로)
    localStorage.setItem('file', JSON.stringify(e.target.result))
}

function uploadImg(e){
    const file = e.target.files[0]
    const reader = new FileReader() // 사용자가 업로로드한 파일 데이터를 읽어오기 위한 파일 객체

    if(!isValid(file.type)){
        imgBox.innerHTML = 'File type is not valid !'
        return;
    }

    const src = URL.createObjectURL(file)
    displayImg(src) // 화면에 이미지를 보여주기

    reader.onload = rememberImg // 파일 읽기가 끝나면 rememberImg 를 실행함
    reader.readAsDataURL(file) // reader 객체가 파일을 읽어오기
}

// 화면이 처음 로딩될때 로컬스토리지에 저장된 이미지를 보여주기
function renderImg(){
    const fileStored = JSON.parse(localStorage.getItem('file'))
    if(fileStored){
        displayImg(fileStored)
    }
}

fileInput.addEventListener('change', uploadImg)
window.addEventListener('load', renderImg)

app.js 파일을 위와 같이 작성하자! 기존에 파일 업로드 처리하기 수업에서 사용된 예제 코드를 활용하기로 한다. 

function uploadImg(e){
    const file = e.target.files[0]
    const reader = new FileReader() // 사용자가 업로로드한 파일 데이터를 읽어오기 위한 파일 객체

    if(!isValid(file.type)){
        imgBox.innerHTML = 'File type is not valid !'
        return;
    }

    const src = URL.createObjectURL(file)
    displayImg(src) // 화면에 이미지를 보여주기

    reader.onload = rememberImg // 파일 읽기가 끝나면 rememberImg 를 실행함
    reader.readAsDataURL(file) // reader 객체가 파일을 읽어오기
}

사용자가 파일을 업로드하면 실행되는 함수이다. 

const reader = new FileReader()

reader 는 업로드한 파일을 읽어들이기 위한 파일 리더 객체이다. 

const src = URL.createObjectURL(file)

file 은 사용자가 업로드한 파일 데이터이다. URL 객체의 내장 메서드인 createObjectURL 메서드를 사용하여 file 의 임시 파일 경로를 Blob 형태로 생성한다. 

displayImg(src) // 화면에 이미지를 보여주기

displayImg 함수에 src 로 정의한 이미지 경로를 인자로 전달하여 화면에 이미지를 보여준다. 

function displayImg(src){
    imgBox.innerHTML = `<img src='${src}'/>`
}

displayImg 함수에는 파라미터로 들어온 src 값을 이용하여 imgBox 에 이미지를 렌더링한다. 

reader.onload = rememberImg

reader 객체에 load 이벤트를 등록하고 rememberImg 라는 이벤트핸들러 함수를 연결해준다. reader 객체가 파일 데이터를 다 읽어들이면 rememberImg 함수가 실행된다. 

reader.readAsDataURL(file)

reader 객체의 readAsDataURL 메서드는 file 데이터를 읽은 다음 base64 문자열 형태의 이미지 경로를 생성한다. 

function rememberImg(e){
    console.log(e.target.result) // reader 객체로 읽어온 데이터  (이미지 경로)
    localStorage.setItem('file', JSON.stringify(e.target.result))
}

reader 객체로부터 읽어온 파일 데이터(base64 문자열)은 e.target.result 로 전달받을 수 있다. 이 값은 이미지 경로로 사용 가능하다. 이 값을 로컬 스토리지에 저장해 두면 나중에 웹페이지를 새로고침해도 이미지를 보여줄 수 있다. 

window.addEventListener('load', renderImg)

window 객체에 load 이벤트를 등록하고 renderImg 이벤트핸들러 함수를 연결한다. 이렇게 하면 웹페이지가 로딩되었을때 renderImg 이벤트핸들러 함수가 실행된다. 

function renderImg(){
    const fileStored = JSON.parse(localStorage.getItem('file'))
    if(fileStored){
        displayImg(fileStored)
    }
}

로컬 스토리지에서 file 이라는 key 값을 사용하여 브라우저에 저장된 이미지 경로를 읽어온다. 해당 경로로 이미지를 화면에 보여준다. 

 

* 브라우저 객체 모델 

브라우저 객체는 브라우저 자체에 내장된 객체이다. window 객체는 브라우저 객체의 최상단에 존재한다. window 객체의 하위 객체로는 document, screen, location, history, navigator 가 존재한다. 각 하위 객체에는 브라우저에서 제공하는 기본적인 프로퍼티와 메서드가 있다. 이렇게 브라우저에 내장된 계층적인 구조의 객체를 브라우저 객체 모델이라고 한다. 

 

window 객체 

브라우저의 최상위 객체이다. 해당 객체의 메서드는 아래와 같다. 

alert(메세지)

window.alert 는 브라우저에서 제공하는 기본 팝업창이다. 메세지는 문자열 형태로 넣어준다. 

prompt(안내문구, 디폴트값)

window.prompt 는 사용자로부터 입력을 받는 팝업창이다. 안내문구는 사용자에게 안내할 가이드로써 문자열 형태로 넣어준다. 디폴트값은 사용자 입력이 없을시 사용할 기본값이다. 

confirm(메세지)

window.confirm 은 사용자로부터 메세지에 대한 응답을 전달받는 팝업창이다. 메세지는 응답을 받을 질의내용이다. 

moveTo(x, y)

window.moveTo 는 현재 브라우저 창을 지정한 위치(x, y) 로 이동한다.  x 는 이동할 수평위치, y 는 이동할 수직위치이다. 

resizeTo(width, height)

window.resizeTo 는 현재 브라우저 창의 크기를 변경한다. width 는 변경하고 싶은 창의 너비, height 은 변경하고 싶은 창의 높이를 의미한다. 

 

window 객체의 메서드를 이용하여 간단한 산수문제 사이트를 만들어보자! 

산수문제 사이트

<!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 class="quiz-container">
    <!-- <p class="question">3 + 3 = ?</p>
    <ol>
      <li>7</li>
      <li>2</li>
      <li>-6</li>
      <li>6</li>
    </ol> -->
  </div>
  <script src='app.js'></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

body{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
h1{
  text-align: center;
}
.quiz-container{
  max-width: 170px;
  margin: 70px auto;
  border: 3px solid #eee;
  position: relative;
}
ol{
  padding: 30px;
  list-style-type: lower-alpha;
}
ol li{
  font-size: 1.1rem;
}
.question{
  font-weight: bold;
  font-style: italic;
  font-size: 1.3rem;
  padding: 10px;
  border-bottom: 3px solid #eee;
}
input[type="button"]{
  background: #eee;
  outline: none;
  border: none;
  padding: 10px;
  cursor: pointer;
  
  position: absolute;
  right: 0;
  bottom: 0;
}

style.css 파일을 위와 같이 작성하자!

const quizContainer = document.querySelector('.quiz-container')
let index = 0 // 문제 번호 
let score = 0 // 점수 

const mathProblems = [ // 수학문제 리스트 
  {
    query: '3 + 3 = ?',
    examples: [7, 2, -6, 6],
    answer: 6
  },
  {
    query: '9 * 4 = ?',
    examples: [30, 13, 36, 34],
    answer: 36
  },
  {
    query: '18 - 31 = ?',
    examples: [-9, 13, -2, -13],
    answer: -13
  },
  {
    query: '28 / 2 = ?',
    examples: [13, 14, 0, 8],
    answer: 14
  },
  {
    query: '(3 + 4) * 7 = ?',
    examples: [49, 14, 21, 7],
    answer: 49
  }
]

function showPrompt(problem){
  const answer = prompt(`${problem.query} 문제에 대한 답을 입력해주세요 !`, 0) // 사용자로부터 답변 묻기
  console.log(answer, typeof answer)

  if(parseInt(answer) === problem.answer){ // 정답을 맞춘 경우 
    score += 20 // 점수 업데이트 
  }else{
    alert('정답이 아닙니다 !')
  }
  index++ // 다음 문제 선택을 위한 문제 번호 증가 
  
  if(index > mathProblems.length - 1){ // 더이상 보여줄 문제가 없으면 재귀함수 종료하기 
    alert(`당신의 최종 점수는 ${score}점입니다.`)
    return 
  }else{
    displayQuiz(mathProblems[index]) // 보여줄 문제가 남아있으면 문제 보여주기 
  }

}

function displayQuiz(problem){
  quizContainer.innerHTML = ` 
    <p class="question">${problem.query}</p>
    <ol>
      <li>${problem.examples[0]}</li>
      <li>${problem.examples[1]}</li>
      <li>${problem.examples[2]}</li>
      <li>${problem.examples[3]}</li>
    </ol>
    <input type="button" value="정답 입력하기" id="answer-btn"/>
  `
  document.getElementById("answer-btn")
  .addEventListener('click', () => showPrompt(problem)) // 답변창을 열기 위한 이벤트핸들러 함수 연결하기 
}

displayQuiz(mathProblems[index])

app.js 파일을 위와 같이 작성하자! 

const quizContainer = document.querySelector('.quiz-container')

수학문제와 선택지를 화면에 동적으로 렌더링하기 위하여 해당 컨테이너를 가져온다. 

let index = 0 // 문제 번호 
let score = 0 // 점수

문제 번호를 이용하여 실제 문제에 대한 데이터에 접근하기 위하여 index 변수를 이용한다. 수학문제 풀이에 대한 총점을 보여주기 위하여 score 변수를 선언한다. 

const mathProblems = [ // 수학문제 리스트 
  {
    query: '3 + 3 = ?',
    examples: [7, 2, -6, 6],
    answer: 6
  },
  {
    query: '9 * 4 = ?',
    examples: [30, 13, 36, 34],
    answer: 36
  },
  {
    query: '18 - 31 = ?',
    examples: [-9, 13, -2, -13],
    answer: -13
  },
  {
    query: '28 / 2 = ?',
    examples: [13, 14, 0, 8],
    answer: 14
  },
  {
    query: '(3 + 4) * 7 = ?',
    examples: [49, 14, 21, 7],
    answer: 49
  }
]

수학문제와 선택지 보기, 그리고 정답에 대한 데이터를 담고 있는 수학문제 리스트를 배열로 정의한다.  query 는 수학문제, examples 는 문제에 대한 보기, answer 는 정답이다. 

displayQuiz(mathProblems[index])

첫번째 문제를 화면에 보여준다. 

function displayQuiz(problem){
  quizContainer.innerHTML = ` 
    <p class="question">${problem.query}</p>
    <ol>
      <li>${problem.examples[0]}</li>
      <li>${problem.examples[1]}</li>
      <li>${problem.examples[2]}</li>
      <li>${problem.examples[3]}</li>
    </ol>
    <input type="button" value="정답 입력하기" id="answer-btn"/>
  `
  document.getElementById("answer-btn")
  .addEventListener('click', () => showPrompt(problem)) // 답변창을 열기 위한 이벤트핸들러 함수 연결하기 
}

수학문제 리스트 배열에서 각 문제에 대한 데이터를 담고 있는 객체(problem)를 파라미터로 받아서 quizContainer 요소에 해당 문제를 렌더링한다. 

<input type="button" value="정답 입력하기" id="answer-btn"/>
document.getElementById("answer-btn")
  .addEventListener('click', () => showPrompt(problem)) // 답변창을 열기 위한 이벤트핸들러 함수 연결하기

정답을 입력할 수 있는 버튼을 만들고, 클릭하면 정답을 입력할 수 있는 화면을 보여준다. 

function showPrompt(problem){
  const answer = prompt(`${problem.query} 문제에 대한 답을 입력해주세요 !`, 0) // 사용자로부터 답변 묻기
  console.log(answer, typeof answer)

  if(parseInt(answer) === problem.answer){ // 정답을 맞춘 경우 
    score += 20 // 점수 업데이트 
  }else{
    alert('정답이 아닙니다 !')
  }
  index++ // 다음 문제 선택을 위한 문제 번호 증가 
  
  if(index > mathProblems.length - 1){ // 더이상 보여줄 문제가 없으면 재귀함수 종료하기 
    alert(`당신의 최종 점수는 ${score}점입니다.`)
    return 
  }else{
    displayQuiz(mathProblems[index]) // 보여줄 문제가 남아있으면 문제 보여주기 
  }
}

showPrompt 에서는 정답 입력을 위한 화면을 보여주고, 사용자가 답을 입력하면 정답인지 아닌지 알려준다.

const answer = prompt(`${problem.query} 문제에 대한 답을 입력해주세요 !`, 0) // 사용자로부터 답변 묻기
console.log(answer, typeof answer)

window.prompt 메서드를 이용하여 답을 입력할 수 있는 화면을 보여준다. prompt 의 두번째 인자는 화면에 보여줄 초기값이다. 사용자가 아무것도 입력하지 않으면 answer 변수에 0이 저장된다. 

if(parseInt(answer) === problem.answer){ // 정답을 맞춘 경우 
    score += 20 // 점수 업데이트 
}else{
    alert('정답이 아닙니다 !')
}

 정답이면 score 변수를 업데이트하고, 정답이 아니면 alert 경고창을 띄워서 정답이 아님을 사용자에게 알려준다. prompt 로 입력받은 answer 값은 문자열이므로 실제 정답과 비교하기 위하여 parseInt 함수를 이용하여 정수로 바꿔준다. 

index++ // 다음 문제 선택을 위한 문제 번호 증가

수학 리스트 배열에서 다음 문제에 대한 데이터를 선택하기 위하여 index 값을 1 증가시켜준다. 

if(index > mathProblems.length - 1){ // 더이상 보여줄 문제가 없으면 재귀함수 종료하기 
    alert(`당신의 최종 점수는 ${score}점입니다.`)
    return 
}else{
    displayQuiz(mathProblems[index]) // 보여줄 문제가 남아있으면 문제 보여주기 
}

else 구문은 보여줄 문제가 남아있는 경우이다. 이때는 displayQuiz 함수와 업데이트된 index 값을 이용하여 다음문제를 화면에 보여준다. displayQuiz 함수에서 showPrompt 를 우회하여 displayQuiz 함수를 다시 호출하므로 재귀적으로 동작한다. 만약 index 값이 수학 리스트 배열의 마지막 인덱스보다 커지면 더이상 보여줄 문제가 없는 경우이므로 이때는 문제풀이 총점을 alert 창에 띄워서 보여주고, 재귀적인 동작을 멈추기 위하여 return 을 적용한다. 

const quizContainer = document.querySelector('.quiz-container')
let index = 0 // 문제번호
let score = 0

const mathProblems = [ // 수학문제 리스트
  {
    query: '3 + 3 = ?',
    examples: [7, 2, -6, 6],
    answer: 6
  },
  {
    query: '9 * 4 = ?',
    examples: [30, 13, 36, 34],
    answer: 36
  },
  {
    query: '18 - 31 = ?',
    examples: [-9, 13, -2, -13],
    answer: -13
  },
  {
    query: '28 / 2 = ?',
    examples: [13, 14, 0, 8],
    answer: 14
  },
  {
    query: '(3 + 4) * 7 = ?',
    examples: [49, 14, 21, 7],
    answer: 49
  }
]

function showPrompt(problem){
  const answer = prompt(`${problem.query} 문제에 대한 답을 입력해주세요 !`, 0) // 사용자로부터 답변 묻기
  console.log(answer, typeof answer)

  if(parseInt(answer) === problem.answer){
    score += 20
  }else{
    alert('정답이 아닙니다!')
  }
  index++
  if(index > mathProblems.length - 1){
    // document.getElementById("answer-btn").removeEventListener('click', () => showPrompt(problem))
    document.getElementById("answer-btn").disabled = true 

    alert(`당신의 최종 점수는 ${score}점입니다.`)
    console.log(document.getElementById("answer-btn"))
    
    return;
  }else{
    displayQuiz(mathProblems[index])
  }
}


function displayQuiz(problem){
  quizContainer.innerHTML = `
    <p class="question">${problem.query}</p>
    <ol>
      ${problem.examples.map(ex => `<li>${ex}</li>`).join('')}
    </ol>
    <input type="button" value="정답 입력하기" id="answer-btn">
  `
  document.getElementById("answer-btn")
  .addEventListener('click', () => showPrompt(problem))
}



displayQuiz(mathProblems[index])

더이상 보여줄 문제가 없을때 클릭 이벤트를 해제하려고 하였으나 잘 동작하지 않아서 아래와 같이 버튼을 비활성화시켰다.

document.getElementById("answer-btn").disabled = true

 

 

location 객체 참고문서

 

Location - Web API | MDN

Location 인터페이스는 객체가 연결된 장소(URL)를 표현합니다.

developer.mozilla.org

 

location 객체 

location 객체는 웹사이트 주소(URL)에 대한 정보를 담고 있다. 웹사이트 주소(URL)에 대한 자세한 정보를 조회하기 위하여 해당 객체에는 아래 예제와 같이 다양한 프로퍼티를 제공한다. 

<!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>
  <a id="link" href='https://developer.mozilla.org:8080/en-US/search?q=URL#search-results-close-container'></a>
  <script src='app.js'></script>
</body>
</html>

index.html 파일을 위와 같이 작성하자!

const link = document.getElementById('link')

const locationInfo = {
  href: link.href,         // https://developer.mozilla.org:8080/en-US/search?q=URL#search-results-close-container
  protocol: link.protocol, // https:
  host: link.host,         // developer.mozilla.org:8080
  hostname: link.hostname, // developer.mozilla.org
  port: link.port,         // 8080
  pathname: link.pathname, // /en-US/search
  search: link.search,     // ?q=URL
  hash: link.hash,         // #search-results-close-container
  origin: link.origin,     // https://developer.mozilla.org:8080
}
console.table(locationInfo)

app.js 파일을 위와 같이 작성하자! 

href: link.href, // https://developer.mozilla.org:8080/en-US/search?q=URL#search-results-close-container

href 프로퍼티는 웹사이트의 전체 URL 주소를 의미한다. 

host: link.host, // developer.mozilla.org:8080

host 프로퍼티는 웹사이트의 Base URL 이다. Base URL 은 API 서버에서는 디폴트 URL 주소를 의미한다. 

pathname: link.pathname, // /en-US/search

pathname 프로퍼티는 서버로부터 리소스를 가져오기 위한 URL 경로이다. 리액트에서도 해당 프로퍼티를 사용할 것이므로 잘 기억해두자!

search: link.search, // ?q=URL

search 프로퍼티는 쿼리스트링(querystring) 문자열을 조회한다. 쿼리스트링은 URL 주소에서 물음표(?) 다음에 나오는 값으로 해당 값을 서버로 보내서 관련된 리소스를 조회한다. 리액트에서도 해당 프로퍼티를 사용할 것이므로 잘 기억해두자!

hash: link.hash,  // #search-results-close-container

hash 프로퍼티는 URL 주소에서 샵(#) 뒤에 따라오는 해쉬 문자열을 조회한다. 해쉬를 이용하여 URL 주소를 변경하거나 화면을 이동할 수 있다. 

location 객체의 프로퍼티 출력 화면

 

history 객체 참고문서

 

History - Web API | MDN

History 인터페이스는 브라우저의 세션 기록, 즉 현재 페이지를 불러온 탭 또는 프레임의 방문 기록을 조작할 수 있는 방법을 제공합니다.

developer.mozilla.org

 

history 객체 

history 객체는 사용자가 방문한 웹사이트에 대한 기록을 저장해두었다가 이전이나 다음 페이지로 손쉽게 이동할 수 있도록 해준다. 물론 a 태그를 이용하면 페이지 이동이 가능하지만 페이지가 새로고침된다. history 객체를 활용하면 새로고침없이 페이지를 이동할 수 있다. 

History.length

length 프로퍼티는 사용자가 방문한 웹페이지의 총 횟수를 의미한다. 

History.state

state 프로퍼티는 사용자가 가장 최근에 방문한 웹페이지에 대한 정보를 담고 있다. 

History.back()

back 메서드는 사용자가 직전에 방문한 웹페이지로 이동한다. 

History.forward()

forward 메서드는 사용자가 그 다음에 방문한 웹페이지로 이동한다. 

History.go(정수)

go 메서드는 back, forward 의 기능을 합쳐놓은 형태이다. 인자로 주어진 정수에 따라 이전 또는 다음 웹페이지로 이동한다. 0 보다 작으면 이전 웹페이지, 0 보다 크면 다음 웹페이지로 이동한다. -2 이면 사용자가 두단계 전에 방문한 웹페이지로 곧바로 이동한다. 

history.pushState(state, title[, url]);

pushState 메서드는 사용자가 방문한 현재 웹페이지에 대한 정보(state)를 스택에 추가하여 기록해둔다. title 은 웹페이지 정보에 대한 닉네임 정도로 이해하면 된다. 보통은 빈 문자열로 둔다. url 은 스택에 저장하고 싶은 웹페이지의 URL 정보이다. 

<!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="page-link">home</button>
  <button class="page-link">about</button>
  <button class="page-link">contact</button>
  <div id="root"></div>

  <script src='app.js'></script>
</body>
</html>

index.html 을 위와 같이 작성하자! 페이지를 이동하기 위한 버튼 3개를 정의힌다. 또한 페이지 컨텐츠를 표시하기 위하여 div 요소를 추가한다. 

const links = document.querySelectorAll('.page-link') // 페이지 이동 버튼 
const root = document.getElementById('root') // 페이지 내용을 담을 컨테이너 요소

const pages = { // 버튼 클릭시 이동할 페이지 정의 
  home: "<h1>Home page</h1><button class='detail-link'>detail</button>",
  about: "<h1>About page</h1>",
  contact: "<h1>Contact page</h1>",
  detail: "<h1>Detail page</h1><button class='order-link'>order</button>",
  order: "<h1>Order page</h1>"
}

function addEvent(currentPage){
  if(currentPage === 'home'){
    const detailLink = document.querySelector('.detail-link')
    detailLink.addEventListener('click', storePageInfo) // 재귀적으로 실행
  }else if(currentPage === 'detail'){
    const orderLink = document.querySelector('.order-link')
    orderLink.addEventListener('click', storePageInfo) // 재귀적으로 실행
  }
}

function storePageInfo(e){
  const currentPage = e.target.innerText // 버튼 클릭으로 이동할 페이지 이름(home, about, contact, detail, order)
  console.log("현재 페이지: ", currentPage) 

  history.pushState({ 'page_title': currentPage }, '', `/${currentPage}`) // 새로고침 없이 URL 주소변경
  root.innerHTML = pages[currentPage] // 페이지 내용 변경

  addEvent(currentPage) // 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 등록
}

for(let link of links){
  link.addEventListener('click', storePageInfo)
}

window.onpopstate = function(e){
  console.log(e.state) // 이동하려는 페이지에 대한 state 객체 

  if(e.state === null){ // URL 주소가 / 인 경우 
    root.innerHTML = '' // 화면 초기화
    return // 함수 종료
  }

  const currentPage = e.state['page_title'] // 이동하려는 이전 또는 다음 페이지 
  root.innerHTML = pages[currentPage] // 페이지 내용 변경
  addEvent(currentPage) // 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 등록
}

app.js 파일을 위와 같이 작성하자!

const links = document.querySelectorAll('.page-link') // 페이지 이동 버튼

페이지 이동을 위하여 클릭 이벤트를 적용할 버튼을 가져온다. 

const root = document.getElementById('root') // 페이지 컨텐츠를 담을 컨테이너 요소

페이지 컨텐츠를 보여주기 위하여 div 요소를 가져온다. 

const pages = { // 버튼 클릭시 이동할 페이지 정의 
  home: "<h1>Home page</h1><button class='detail-link'>detail</button>",
  about: "<h1>About page</h1>",
  contact: "<h1>Contact page</h1>",
  detail: "<h1>Detail page</h1><button class='order-link'>order</button>",
  order: "<h1>Order page</h1>"
}

사용자가 버튼을 클릭할때 이동할 페이지를 정의힌다. pages 객체의 프로퍼티 이름(home, about, ...)은 페이지 이름과 동일히다. 프로퍼티 값(<h1>About page</h1>, ...)은 해당 페이지에서 보여줄 내용이다. 

for(let link of links){
  link.addEventListener('click', storePageInfo)
}

각 버튼에 click 이벤트가 발생할때 storePageInfo 라는 이벤트핸들러 함수가 실행되게 한다. 

function storePageInfo(e){
  const currentPage = e.target.innerText // 버튼 클릭으로 이동할 페이지 이름(home, about, contact, detail, order)
  console.log("현재 페이지: ", currentPage) 

  history.pushState({ 'page_title': currentPage }, '', `/${currentPage}`) // 새로고침 없이 URL 주소변경
  root.innerHTML = pages[currentPage] // 페이지 내용 변경

  addEvent(currentPage) // 이동할 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 연결
}

storePageInfo 함수는 클릭한 페이지로 이동한다. 또한, 해당 페이지 내부에 버튼이 존재하면 해당 버튼에도 동일한 클릭 이벤트를 연결한다. 

const currentPage = e.target.innerText // 버튼 클릭으로 이동할 페이지 이름(home, about, contact, detail, order)
console.log("현재 페이지: ", currentPage)

currentPage 는 이동하려는 페이지 이름이다. 

history.pushState({ 'page_title': currentPage }, '', `/${currentPage}`) // 새로고침 없이 URL 주소변경

새로고침없이 이동하려는 페이지로 URL 주소를 변경한다. 변경하려는 URL 주소는 pushState 의 세번째 인자에 설정하면 된다. 동시에 이동하려는 페이지 이름을 'page_title' 이라는 프로퍼티에 저장한다. 첫번째 인자로 주어진 state 객체는 추후에 사용자가 웹사이트의 이전 또는 다음 페이지로 이동할때 사용된다. state 객체는 메모리에 스택처럼 쌓인다. 

root.innerHTML = pages[currentPage] // 페이지 내용 변경

페이지 이름(currentPage)을 이용하여 pages 객체에서 해당 페이지에 보여줄 내용을 조회한다. 그런 다음 해당 페이지 내용을 root 요소에 렌더링한다. 

addEvent(currentPage) // 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 등록

이동하려는 페이지 내부에도 버튼이 존재하는 경우 해당 버튼에도 동일한 클릭 이벤트를 등록한다. 

function addEvent(currentPage){
  if(currentPage === 'home'){
    const detailLink = document.querySelector('.detail-link')
    detailLink.addEventListener('click', storePageInfo) // 재귀적으로 실행
  }else if(currentPage === 'detail'){
    const orderLink = document.querySelector('.order-link')
    orderLink.addEventListener('click', storePageInfo) // 재귀적으로 실행
  }
}

addEvent 함수는 이동하려는 페이지의 이름이 home 또는 detail 인 경우에 해당 페이지 내부에 존재하는 버튼에 클릭 이벤트를 등록하고, storePageInfo 라는 이벤트핸들러 함수를 연결한다. 이렇게 하면 storePageInfo 에서 addEvent 함수를 우회하여 다시 storePageInfo 를 호출한 것이므로 재귀적으로 동작한다. 즉, storePageInfo 함수는 재사용된다. 

window.onpopstate = function(e){
  console.log(e.state) // 이동하려는 페이지에 대한 state 객체 

  if(e.state === null){ // URL 주소가 / 인 경우 
    root.innerHTML = '' // 화면 초기화
    return // 함수 종료
  }

  const currentPage = e.state['page_title'] // 이동하려는 이전 또는 다음 페이지 
  root.innerHTML = pages[currentPage] // 페이지 내용 변경
  addEvent(currentPage) // 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 등록
}

사용자가 브라우저에서 이전 또는 다음 페이지로 이동할때 해당 이벤트핸들러 함수가 실행된다. 

if(e.state === null){ // URL 주소가 / 인 경우 
    root.innerHTML = '' // 화면 초기화
    return // 함수 종료
 }

이동하려는 페이지의 URL 주소가 / 이면 index.html 파일이므로 페이지 내용은 보이지 않게 root 요소의 컨텐츠를 초기화한다. 

const currentPage = e.state['page_title'] // 이동하려는 이전 또는 다음 페이지

currentPage 는 버튼 클릭시(사용자가 페이지 방문시) history 객체의 pushState 메서드로 저장한 state 객체를 조회한다. state 객체는 이동하려는 페이지를 의미한다. 

root.innerHTML = pages[currentPage] // 페이지 내용 변경
addEvent(currentPage) // 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 클릭 이벤트 등록

웹화면을 이동하려는 페이지 내용으로 변경한다. 또한, 이동하려는 페이지 내부에 버튼이 존재하는 경우 해당 버튼에도 동일한 클릭 이벤트를 등록한다. 

history 객체로 구현한 간단한 페이지 이동 화면

 

 

 

 

 

 

 

JSON 객체 (객체의 JSON변경)

폼(Form) 조작

 navigator 객체

 

 

* 연습과제 1

컨텐츠에 이미지를 추가하고, 스크롤 이벤트의 up 클래스를 적용하여 컨텐츠가 위로 올라가는 애니메이션을 적용해보자!

 

* 연습과제 2

컨텐츠에 이미지를 추가하고, 스크롤 이벤트의 down 클래스도 함께 적용하여 컨텐츠가 위아래로 움직이는 애니메이션을 적용해보자!

 

* 연습과제 3

컨텐츠에 이미지를 추가하고, 스크롤 이벤트의 left, right 클래스도 함께 적용하여 컨텐츠가 위아래 좌우로 움직이는 애니메이션을 적용해보자!

 

* 연습과제 4

슬라이드 플립(flip) 예제에서 Y축이 아니라 X축을 기준으로 플립되도록 코드를 수정해보자!

 

* 연습과제 5

로컬 스토리지 예제에서 세션 스토리지에 데이터를 저장하도록 코드를 변경해보고, 차이점을 이해해보자!

 

* 연습과제 6

로컬 스토리지 예제에서 다수의 이미지 파일을 업로드하고 로컬 스토리지에 저장하여 새로고침하더라도 업로드한 사진이 남아있도록 해보자! 개략적인 로직은 아래와 같다.

// 사용자가 파일을 업로드할때 아래와 같은 순서로 파일 저장하기

1) 로컬 스토리지에서 이미지 데이터들이 저장된 배열 읽어오기
2) 배열에 사용자가 업로드하는 이미지 데이터 추가하기 => 배열의 push 메서드 사용
3) 로컬 스토리지에 업데이트된 배열 저장하기 


// 화면이 새로 렌더링될때 아래와 같은 순서로 파일 읽어오기

1) 로컬 스토리지에서 이미지 데이터들이 저장된 배열 읽어오기
2) 각 이미지 데이터를 조회하면서 화면에 디스플레이하기

 

* 연습과제 7

매트릭스 텍스트 애니메이션 코드를 응용하여 아래 영상처럼 마치 물방울이 바닥에 닿으면서 튀는것과 같은 애니메이션을 구현해보세요!

 

* 연습과제 8

스크롤 애니메이션 예제를 참고하여 아래와 같이 스크롤시 다음 슬라이드가 이전 슬라이드를 밀어내면서 왼쪽으로 이동하도록 해보자!

 

* 연습문제 9

아래와 같이 스크롤시 위아래로 내려온 후 클릭시 확장되는 슬라이드를 구현해보세요!

 

728x90