(Javascript) 2. 자기소개 페이지 -2- : 쓰다보니 전체 내용

김동우·2021년 6월 17일
3

wecode

목록 보기
11/32

잠깐! 시작하기 전에

이 글은 wecode 사전 스터디에서 실제 공부하고, 이해한 내용들을 적는 글입니다. 글의 표현과는 달리 어쩌면 실무와는 전혀 상관이 없는 글일 수 있습니다.

또한 해당 글은 다양한 자료들과 작성자 지식이 합성된 글입니다. 따라서 원문의 포스팅들이 틀린 정보이거나, 해당 개념에 대한 작성자의 이해가 부족할 수 있습니다.

설명하듯 적는게 습관이라 권위자 발톱만큼의 향기가 조금은 날 수 있으나, 엄연히 학생입니다. 따라서 하나의 참고자료로 활용하시길 바랍니다.

글의 내용과 다른 정보나 견해의 차이가 있을 수 있습니다.
이럴 때, 해당 부분을 언급하셔서 제가 더 공부할 수 있는 기회를 제공해주시면 감사할 것 같습니다.

Javascript 구성

잠깐! 해당 글을 보시기 전에 파일을 참고해주시면 도움이 됩니다.

project - github repo

자기소개 페이지 다시보기

main.js

main.js의 경우 즉시실행함수로 둘러싸여있는 구조로 되어있습니다.

해당 함수를 function init() 혹은 다양한 이름들로 선언함과 동시에 실행해도 괜찮지만, 저는 그냥 익명함수로 실행시켰습니다.


import { screenInit } from "./screen.js";

(() => {
  screenInit();
  window.addEventListener(`resize`, () => {
    screenInit();
  });
  window.addEventListener(`orientationchange`, () => {
    screenInit();
  });
  // console.log(window.sessionStorage);
})();

해당 블럭이 main.js의 전부입니다. addEnetListner() 의 경우 window와 document 내부에 존재하는 메서드입니다.

사용할 수 있는 Event의 종류가 다르니, 공식문서를 꼭 활용하시는 것을 추천드립니다.

또한, import, export는 지금은 신경쓰지 않도록 합시다. 추후 개발환경에 관한 얘기를 나눌 때, CORS 이슈와 함께 다루도록 하겠습니다.

addEventListner()

해당 블럭 내에는 css 의 @media screen and와 마찬가지로 viewport의 크기가 달라지거나, 모바일 화면에서 방향전환(orientationChange) 시 screen을 변경해주는 코드(screenInit을 실행, callback function)가 들어있다고 생각하시면 될 것 같습니다.

여기서
1. add( 추가 )
2. event( resize )
3. listner( ( ) => { } )

개념을 분리해서 생각해봅시다.

add의 경우 간단합니다. 앞으로 브라우저가 인식하도록 추가하겠다. 정도로 생각할 수 있습니다.

event 또한 window, 창 내에서 발생하는 하나의 현상을 의미합니다.

listner는 후속조치를 의미합니다.

즉, listner는 event에 귀를 기울이고 있습니다. 그리고 이후 작성된 중괄호({ }) 내부에 존재하는 특정 행동양식을 그대로 수행하게 됩니다.

예를 들어, 구조요원이 물에 빠진 사람을 구하기 위해 바다에 뛰어들기 위해서는 최소 3개의 과정을 겪어야 한다고 가정해봅시다.

이를 addEventListner와 같이 생각해보겠습니다.

  1. 바다(window)를 감시한다. -> 실행
  2. 저 멀리 행동이 부자연스러운 사람을 발견한다.(event 발생) -> 발생
  3. 숙지하고 있는 방법(()=>{수영(); 보트출동();})으로 신속히 구조한다. -> 후속조치

이런 구조로 이루어진 메서드임을 이해하고 넘어가봅시다.

또한, 익명함수로 screenInit을 둘러싸야 하는 이유에 대해 MDN에서는, 캡슐화되어 있다는 표현을 사용합니다. 이런 내용은 후에 객체지향을 정리할 때 추가적으로 적을 수 있지 않을까 생각합니다. 해당 포스팅에서는 무시하겠습니다.

이제 다음 파일을 살펴보는데, Javascript 모듈화에 따라 페이지 기능을 담당하는 로직들을 우선적으로 보겠습니다.

scrollAnimation.js

다양한 애니메이션 로직이 담겨있는 파일입니다. 해당 파일의 설명은 주석으로 대체하도록 하겠습니다.


// 서서히 나타남
// 구간 내에서 scroll 값에 의해 특정 htmlElements의 opacity가 스크롤 값에 의해 0부터 증가하게 됩니다.
export function fadeIn(htmlElements, scrollValue, minNum, maxNum) {
  htmlElements.style.opacity = (scrollValue - minNum) / (maxNum - minNum);
}

// 서서히 사라짐
// 구간 내에서 scroll 값에 의해 특정 htmlElements의 opacity가 스크롤 값에 의해 1부터 감소하게 됩니다.
export function fadeOut(htmlElements, scrollValue, minNum, maxNum) {
  htmlElements.style.opacity = (maxNum - scrollValue) / (maxNum - minNum);
}

// 서서히 배경색 검정으로 변함
// 구간 내에서 scroll 값에 의해 특정 htmlElements의 backgroundColor가 white에서 black으로 서서히 변경됩니다.(투명도는 0에서 1로)
export function whiteToBlack(htmlElements, scrollValue, minNum, maxNum) {
  let zeroToOne = (scrollValue - minNum) / (maxNum - minNum);
  htmlElements.style.backgroundColor = `rgba(0,0,0,${zeroToOne})`; 
  // 해당 값으로 색상 결정이 가능합니다.
  // console.log(zeroToOne);
}

// 서서히 배경색 흰색으로 변함
// 구간 내에서 scroll 값에 의해 특정 htmlElements의 backgroundColor가 black 값에서 white값으로 서서히 변경됩니다.(투명도는 1에서 0으로)
export function blackToWhite(htmlElements, scrollValue, minNum, maxNum) {
  let oneToZero = (maxNum - scrollValue) / (maxNum - minNum);
  htmlElements.style.backgroundColor = `rgba(0,0,0,${oneToZero})`; 
  // 해당 값으로 색상 결정이 가능합니다.
}

// 서서히 나타났다가 다시 사라짐
// 구간 내에서 scroll 값에 의해 특정 htmlElements의 opacity가 0에서 1, 다시 1에서 0으로 감소합니다.
export function scrollOpacity(htmlElements, scrollValue, minNum, maxNum) {
  if (scrollValue >= minNum && scrollValue < maxNum) {
    if (scrollValue - minNum < (maxNum - minNum) / 2) {
      htmlElements.style.opacity =
        ((scrollValue - minNum) / (maxNum - minNum)) * 2;
    } else {
      htmlElements.style.opacity =
        ((maxNum - scrollValue) / (maxNum - minNum)) * 2;
    }
  } else {
    htmlElements.style.opacity = 0;
  }
}

// 이미지 가로길이 상승
// 구간 내에서 스크롤 값에 따라 특정 htmlElements(ex. img)의 가로 길이가 올라갑니다.
export function imgWidthExtend(htmlElements, scrollValue, minNum, maxNum) {
  let imgWidth = ((scrollValue - minNum) / (maxNum - minNum)) * 100 + 50; // 초기값 50을 더해줍니다. (최소 50%가 나오도록, 선택사항)

  if (imgWidth <= 100) {
    htmlElements.style.width = `${imgWidth}` + `%`;
  } else if (imgWidth > 100) {
    htmlElements.style.width = `100%`;
  }
}

// 이미지 세로길이 상승
// 구간 내에서 스크롤 값에 따라 특정 htmlElements(ex. img)의 세로 길이가 올라갑니다.
export function imgHeightExtend(htmlElements, scrollValue, minNum, maxNum) {
  let imgHeight = ((scrollValue - minNum) / (maxNum - minNum)) * 100 + 50; // 초기값 50을 더해줍니다. (최소 50%가 나오도록, 선택사항)
  // console.log(htmlElements.style.height);
  if (imgHeight <= 100) {
    htmlElements.style.height = `${imgHeight}` + `%`;
  } else if (imgHeight > 100) {
    htmlElements.style.height = `100%`;
  }
}

// 이미지 넓이 확장
// 구간 내에서 스크롤 값에 따라 특정 htmlElements(ex. img)의 넓이가 확장됩니다.
// 굳이 분리한 이유는 비율을 따로 control할 수 있는 방식을 지향하고 있기 때문입니다.
export function scrollImgExtend(htmlElements, scrollValue, minNum, maxNum) {
  imgWidthExtend(htmlElements, scrollValue, minNum, maxNum);
  imgHeightExtend(htmlElements, scrollValue, minNum, maxNum);
}

// 체크박스, 버튼 등의 요소 클릭 제한
// 지정된 구간 내, 외에서 특정 htmlElemets의 클릭 기능을 control할 수 있습니다.
export function toVisible(htmlElements, scrollValue, minNum, maxNum) {
  if (scrollValue >= minNum && scrollValue < maxNum) {
    htmlElements.style.visibility = `visible`;
  } else if (scrollValue > maxNum) {
    htmlElements.style.visibility = `visible`;
  } else {
    htmlElements.style.visibility = `hidden`;
  }
}

// Icon translate
// 구간 내에서 스크롤 값에 따라 특정 htmlElements의 translate transform animation이 구현됩니다.
export function moveIcon(
  htmlElements,
  scrollValue,
  minNum,
  maxNum,
  xNum,
  yNum
) {
  let scrollTranslateX = ((scrollValue - minNum) / (maxNum - minNum)) * xNum,
    scrollTranslateY = ((scrollValue - minNum) / (maxNum - minNum)) * yNum;
  if (scrollValue < maxNum && scrollValue >= minNum) {
    htmlElements.style.transform = `translate(${scrollTranslateX}%, ${scrollTranslateY}%)`;
  } else if (scrollValue >= maxNum) {
    htmlElements.style.transform = `translate(${xNum}%, ${yNum}%)`;
  } else {
    htmlElements.style.transform = `translate(0%, 0%)`;
  }
}

// Icon scale
// 구간 내에서 스크롤 값에 따라 특정 htmlElements의 scale transform animation이 구현됩니다.
export function scaleIcon(htmlElements, scrollValue, minNum, maxNum, scaleNum) {
  let scrollScaleNum =
    ((scrollValue - minNum) / (maxNum - minNum)) * scaleNum + 1;
  if (scrollScaleNum >= scaleNum) {
    scrollScaleNum = scaleNum;
  }
  if (scrollValue < maxNum && scrollValue >= minNum) {
    htmlElements.style.transform = `scale(${scrollScaleNum})`;
  } else if (scrollValue >= maxNum) {
    htmlElements.style.transform = `scale(${scaleNum})`;
  } else {
    htmlElements.style.transform = `scale(1)`;
  }
}

// Icon rotate
// 구간 내에서 스크롤 값에 따라 특정 htmlElements의 rotate transform animation이 구현됩니다.
export function rotateIcon(htmlElements, scrollValue, minNum, maxNum, deg) {
  let scrollDeg = ((scrollValue - minNum) / (maxNum - minNum)) * deg;
  if (scrollValue < maxNum && scrollValue >= minNum) {
    htmlElements.style.transform = `rotateZ(${scrollDeg}deg)`;
  } else if (scrollValue >= maxNum) {
    htmlElements.style.transform = `rotateZ(${deg}deg)`;
  } else {
    htmlElements.style.transform = `rotateZ(0)`;
  }
}

이제 이 함수들을 활용해서 scroll.js에서는 다양한 elements 들을 control 합니다.

아직 코드가 지저분하고, 사실 합쳐도 되는 블럭들이 일부 존재합니다.

현재 제가 생각하는 범위 내에서는 최선의 코드라고 생각하지만, 언제고 다시 보면 변경점이 있을 수 있습니다.

지금 글을 쓰는 사이에도 약간 수정할게 보여 수정을 했습니다.

github 에는 현재 글의 코드로 올라갑니다.

이후 중복을 최대한 제거하여 정리된 상태의 코드를 따로 올리거나, 보다 더 세밀하게 조정해야하는 부분들이 나타나면 오히려 더 세분화 할 수 있음을 미리 알려드리겠습니다.

CSS의 경우 그 날 기분에 따라 계속 변경될 수 있습니다. (국룰)

scroll.js

여기도 코드를 우선 보겠습니다.

// 해당 코드의 conole.log(); 부분은 전부 개발 당시 test 지점입니다.

// 모든 로직 가져오기
import * as scrollAnimation from "./scrollAnimation.js";

// container 객체 내에 .main-block .intro, .main-block .second-intro section elements 저장
export const container = {
  firstSection: document.querySelector(`.intro`),
  secondSection: document.querySelector(`.second-intro`),
};

// paint 함수의 경우 init과도 같습니다. 
// 저는 해당 파일 내에서 해당 요소들에 대한 변경점을 시작과 동시에 그려내고, 
// 이후 이벤트에서도 해당 함수를 콜백하여 다시 그리는 방법을 채택했습니다.
// 이에 paint 함수에서는 scroll 값에 의한 다양한 조건문을 가집니다.
// 주의깊게 보셔야 할 점은 scrollEvent 함수 내에서 paint 함수를 실행한다는 점입니다.

function paintFirstIntro(scrollValue) {
  // console.log(document.documentElement.scrollTop);
  // .continer class명을 가진 element를 가져옵니다.
  let canvas = document.querySelector(`.container`),
    introBlock = container.firstSection.children,
    introDesc = introBlock[0].children;
  
  // console.log(introDesc);
  // HTML collection, array의 형태를 띄지만 array는 아닙니다.
  // 그러나 인덱스로 해당 요소에 접근하는 방식을 사용할 수 있습니다.
  // console.log(canvas);

  // scrollValue는, viewport의 맨 위 line, top에 해당하는 값이 됩니다.
  // console.log(document.documentElement);
  // 주석처리된 코드를 실행하면 console 창에서 HTML 요소를 관찰할 수 있습니다.

  // console.log(scrollTop);
  
  // 배경화면 흰색 -> 검정
  scrollAnimation.whiteToBlack(canvas, scrollValue, 100, 400);
  
  // scroll 값 100 이상일 때 인사말 사라짐
  if (scrollValue <= 100) {
    introDesc[0].style.opacity = 1;
  } else {
    introDesc[0].style.opacity = 0;
  }
  
  // scrollOpacity(introDesc[1], scrollValue, 0, 800);
  // scrollOpacity(introDesc[2], scrollValue, 800, 1600);
  // scrollOpacity(introDesc[3], scrollValue, 1600, 2400);
  // scrollOpacity(introDesc[4], scrollValue, 2400, 3200);
  
  // 주석처리된 코드를 for문으로 정리했습니다.
  // 각 span, p 요소들이 순차적으로 나타남-사라짐 반복(fadeIn-Out)
  for (let i = 1; i < 5; i++) {
    scrollAnimation.scrollOpacity(
      introDesc[i],
      scrollValue,
      800 * (i - 1) + 200,
      800 * i + 200
    );
  }
  // 3300 scroll값을 초과할 때 해당 함수를 실행.
  if (scrollValue >= 3300) {
    scrollAnimation.blackToWhite(canvas, scrollValue, 3300, 3400);
  }
}

// scrollEvent
export function introScrollEvent() {
  // console.log(document.documentElement.scrollTop);
  // 문서를 보는 vieport 최상단의 scroll px값(위치값)
  let scrollTop = document.documentElement.scrollTop;
  
  // 문서의 현재 스크롤값으로 한 번 그려냅니다.(중요)
  // reload가 발생해도 스크롤 값 변화가 없어 현재 위치를 유지하게 함.
  paintFirstIntro(scrollTop);
  
  
  document.addEventListener(`scroll`, () => {
    // scroll event가 발생하면 실시간으로 값을 변경해줍니다.
    scrollTop = document.documentElement.scrollTop;
    // 해당 값으로 인해 변경되는 요소들을 다시 그려냅니다. so smart
    paintFirstIntro(scrollTop);
  });
}

// paint function, 스크롤 값을 전달받아 조건에 부합하는 요소들을 그려냅니다.
function paintSecondIntro(scrollValue) {
  let introDesc = container.secondSection.children,
    icons = introDesc[1].children;
  
  // for문으로 요소들의 visibility 속성을 제어합니다.
  for (let i = 0; i < 4; i++) {
    scrollAnimation.toVisible(
      icons[i],
      scrollValue,
      800 * i + 4000,
      800 * (i + 1) + 4000
    );
  }

  // console.log(introDesc[1]);
  if (scrollValue >= 3200) {
    scrollAnimation.fadeIn(introDesc[0], scrollValue, 3200, 3250); // bg image
    scrollAnimation.scrollImgExtend(introDesc[0], scrollValue, 3200, 4400);
    // fadeIn(introDesc[1], scrollValue, 4000, 4800); // icon div
    // for문으로 요소의 나타남을 제어합니다.
    for (let i = 0; i < 4; i++) {
      scrollAnimation.fadeIn(
        icons[i],
        scrollValue,
        800 * i + 4000,
        800 * (i + 1) + 4000
      );
    }
    scrollAnimation.moveIcon(icons[0], scrollValue, 4800, 5600, 100, -80);
    scrollAnimation.scaleIcon(icons[1], scrollValue, 5600, 6400, 1.5);
    scrollAnimation.moveIcon(icons[2], scrollValue, 6400, 7200, 0, -80);
    scrollAnimation.rotateIcon(icons[3], scrollValue, 7200, 8000, 360);
  } else {
    // for (let i = 0; i < 2; i++) {
    //   introDesc[i].style.opacity = 0;
    // }
    introDesc[0].style.opacity = 0;
    for (let i = 0; i < 4; i++) {
      icons[i].style.opacity = 0;
    }
  }
}

// 마찬가지로 scroll 값을 갱신하고, 현재 스크롤 값에 부합하는 요소를 그려냅니다.
export function secondIntroScrollEvent() {
  let scrollTop = document.documentElement.scrollTop;
  // 초기 스크롤 값으로 한 번 그려냅니다.
  paintSecondIntro(scrollTop);
	
  // 갱신된 스크롤 값으로 요소 속성 변경을 적용합니다.
  document.addEventListener(`scroll`, () => {
    scrollTop = document.documentElement.scrollTop;
    paintSecondIntro(scrollTop);
  });
}

코드만 보면 상당히 어지럽습니다.

정말 어질어질하네요... 근데, 사실 크게 생각하면 2가지로 압축할 수 있습니다.

(scrollEvent fucntion 내에서)
1. 초기값으로 그려낸다(paint function call).
2. 스크롤값을 갱신해서 다시 그려낸다(paint function call).

별로 어렵지는 않은 내용인데, 제 코드가 지저분해서 상당히 어려워보이네요.

그래도 실제 함수가 실행되는 부분은 위 scrollEvent function 내부에서만 실행됩니다.

paint function은 일종의 template에 가까운 내용들입니다.

이 정도 길이의 코드가 벌써 지저분하다니... 막막하긴 하네요. 😢

주석 스타일은 그냥 저렇게 통일했습니다. 여러줄의 구문을 한번에 묶어도 되는데, 아무래도 통일이 깨지면 눈이 좀 불편하지 않을까 생각이 들어서 모든 주석의 스타일을 똑같이 할까 생각중입니다.

자, 다음은 screen.js를 보며 글을 마쳐보도록 합시다.

screen.js


// scroll.js에서 생성한 container obj와 2개의 function을 가져옵니다.
import {
  container,
  introScrollEvent,
  secondIntroScrollEvent,
} from "./scroll.js";

// viewport의 너비와 길이에 따라 배경 img의 크기가 결정되는데(비율유지),
// 이 때, 배경 img 위에 존재하는 Icon 들의 position 위치가
// 원치 않는 위치인 것을 극복하기 위한 함수입니다.
function secondIntroTextSizing(htmlElements) {
  // 먼저 브라우저가 해당 페이지를 열 때, 초기의 창의 크기를 구해야 합니다.
  // 이를 각 변수에 저장합니다. 
  let viewportWidth = window.innerWidth, // 가로
    viewportHeight = window.innerHeight; // 세로

  // console.log(viewportHeight);
  // console.log(viewportWidth);
  
  // 제 페이지의 경우 꽉 찬 화면 내에 다양한 Icon들이 위치합니다.
  // 이에 bg-img()의 비율이 유지된 순수한 크기와 
  // Icon div의 크기는 일치해야 합니다.
  // Icon div(.second-intro__text) 의 크기를 구하기 위한
  // 최대값인 100%를 각각 적용시킵니다.(bg-img 가 100%, 100%)
  htmlElements.style.width = `100%`;
  htmlElements.style.height = `100%`;

  // 실제 둘 중 큰 값에 따라 bg-img의 순수한 크기가 결정됩니다.
  // img 가 width : height = 3 : 2 의 비율이기 때문에
  // 이에 맞춰 Icon div 또한 3:2의 비율로 구현합니다.
  if (viewportWidth > viewportHeight * 1.5) {
    htmlElements.style.width = `${
      ((viewportHeight * 1.5) / viewportWidth) * 100
    }%`;
    // console.log(htmlElements.style.width);
  }

  if (viewportHeight > viewportWidth * 0.67) {
    htmlElements.style.height = `${
      ((viewportWidth * 0.67) / viewportHeight) * 100
    }%`;
    // console.log(htmlElements.style.height);
  }
}

// 위 sizing 함수를 가장 먼저 실행하고,
// 후에 scrollEvent에 따른 요소들의 속성을 적용하는 init 함수입니다.
// 이 함수를 다시 main에 가져가서 다른 window event에 적용합니다.
export function screenInit() {
  const secondIntro = container.secondSection.children,
    secondText = secondIntro[1];
  secondIntroTextSizing(secondText);
  introScrollEvent();
  secondIntroScrollEvent();
}

screen.js
.
.
.
main.js


import { screenInit } from "./screen.js";

(() => {
  screenInit();
  window.addEventListener(`resize`, () => {
    screenInit();
  });
  window.addEventListener(`orientationchange`, () => {
    screenInit();
  });
  // console.log(window.sessionStorage);
})();

본의 아니게 글이 수미상관 구조가 되어버렸습니다.

자, main.js 는 상당히 별게 없지만, 실제로 까보면 아주 많은 기능들을 실행하고 있다는 점을 볼 수 있었습니다.

그 결과가 바로 이 녀석 입니다.

괜찮은가요? 아직은 부족한게 많아 보입니다.

마치며

이번 프로젝트를 통해 많은 것들을 시도해보지 않았나 돌아보았습니다. 근데 딱히 많은 것을 시도하지는 않았네요.

애플 홈페이지에서나 보던 스크롤 이벤트들을 직접 함수를 만들어 구현해본 것은 이전에 HTML-CSS공부를 위해 해보았던 블리자드 홈페이지 도둑질과 별개로 더 나아가지 않았나 생각이 듭니다.

(2020.12) 블리자드 홈페이지 도둑질

이 때, CSS animation과 flexbox 개념을 열심히 생각해본게 아닐까 싶습니다.

또한, 이번 프로젝트는 이전 애플 홈페이지 클론 강의를 들으며 구현하려다 포기한 저에게는 상당히 뜻깊은 의미를 가지고 있습니다.

물론 1분코딩 선생님의 애플 클론 코딩의 소스와는 전혀 다를겁니다.

제가 강의를 거의 듣지 않았던게 내심 한심해서, 벌로 혼자 힘으로 만들어야겠다고 다짐했습니다.

그래도 헤딩을 하며 무언가를 만들어보는것, 도전하는 것이 얼마나 즐거운지 알게 되는 프로젝트가 아니었나 싶습니다.

이후에도 더 다양한 요소를 추가할 생각이지만, 지금 당장은 사용했던 것을 정리하며 시간을 보낼까 합니다.

그럼 이번 글은 여기서 마치겠습니다. 긴 글 읽어주셔서 감사합니다.

1개의 댓글

comment-user-thumbnail
2021년 6월 30일

팀오 에이스 화이팅..!

답글 달기