Modern website 2

김동현·2023년 4월 6일
0

개인 프로젝트

목록 보기
7/13
post-thumbnail

목적

웹 페이지에 여러가지 기술들을 적용해 보고 싶었다.

  • 꽉찬 동영상 섹션
  • 이미지 슬라이더
  • 부드러운 스크롤 이동
  • 스크롤 애니메이션

사용한 기술

  • react18+ ( react function components )
  • react-icons
  • IntersectionObserver API
  • 기본 CSS의 flex / grid
  • CSS BEM Model
  • media queries

느낀점

생각보다 구현이 쉬웠던 것이 있는 반면, 별거 아닌줄 알았는데 시간이 오래 걸렸던 것이 있다.

먼저 구현이 쉬웠던 것중 하나는 부드러운 스크롤 이동이다.
부드러운 스크롤 이동은 CSS로 쉽게 해결 가능했다.
scroll-behavior : smooth;

각 센션마다 id를 부여해서 navbar에서 클릭하면 해당 섹션으로 부드럽게 이동한다.
섹션으로 이동할 때 오른쪽 수직 스크롤바가 시선을 강탈하기 때문에 없애려고 했는데 아직 표준화된 방법이 없는 듯하다.

각 브라우저마다 서로 다른 방법으로 스크롤바를 없앨 수 있었다.

/** 크로미움 */
::-webkit-scrollbar {
  display: none;
}
/** 파이어폭스 */
html {
  scrollbar-width: none;
}

희안한 건 위의 코드 중 파이어폭스에서 html 엘리먼트 대신 body 엘리먼트에 정의하면 스크롤이 없어지지 않는다.

애니메이션 구현도 쉬웠다.
다음의 사이트를 이용하자.
CSS 애니메이션 모음집 사이트↗️
위의 사이트에서 각 애니메이션의 프리뷰를 볼 수 있고 커스텀도 할 수 있다.
시각적으로 바로바로 보여주며 사용하고자 하는 애니메이션을 CSS로 만들어 준다.

별거 아닌줄 알았는데 상당히 시간을 쏟은 것 중 하나는 replaced element를 다루는 작업이었다.

이번 프로젝트에는 동영상 섹션이 포함되어 있다.
동영상은 <video> 엘리먼트로 삽입했다.
알다시피 <video> 엘리먼트는 replaced element이다.
동영상을 원본 비율로 가로화면을 꽉 채우고 싶었다.
따라서 해당 동영상의 비율을 찾아내서 containing block에

width: 100vw;
height: 56.25vw;

를 적용하고 <video> 를 설정하면 된다.

하지만 위의 경우의 단점으로, 수직 스크롤바가 존재할 경우 수직 스크롤바의 너비 때문에 화면이 넘치는 현상이 발생할 수 있고 동영상의 비율을 직접 알아내서 하드코딩해야 한다는 점이다.

스크롤바 때문에 화면이 넘치는 현상은

width: 100%;
padding-bottom: 56.25%;

과 같이 회피할 수 있지만, 비율을 하드코딩해야한다는 점은 여전히 문제였다.

replaced element 에 max-width: 100% 를 적용하면 containing block보다 영상의 크기가 작을땐 원본 크기로, containing block보다 영상의 크기가 클 땐 containing block의 너비에 맞춰서 축소된다.
이때 비율은 원본 비율이 유지된다.
따라서 다음과 같이 코드를 수정했다.

.containing-block{
  width: 100%;
}
video {
  max-width: 100%;
}

containing block은 화면을 꽉채우는 가변 너비이기 때문에 width: 100% 로 고정했고 video 엘리먼트는 비율을 유지한채 containing block의 너비에 꽉 맞춰지도록 설정했다.

예상한 동작은 다음과 같았다.

  1. video 엘리먼트가 원본 비율대로 확대 / 축소가 되며 containing block에 맞춰진다.

  2. video 엘리먼트의 높이가 결정되고 video 엘리먼트를 포함하고 있는 containing block의 높이가 video높이만큼 설정된다.

매우 그럴듯 하지만 틀렸다.

버그인지 의도한 건지 모르지만 containing block의 높이가 video 높이보다 더 크다.

containing block의 배경을 회색으로 설정해서 시각적으로 확인해보자.
하단에 회색이 보임

마진이나 패딩 보더와 같은 박스 사이즈의 문제는 아니었다.

확실하게 video의 높이는 containing block보다 작았으며, containing block의 회색부분의 높이는 무엇으로 적용되었는 지 알 수 없었다.

개발자 도구에서도 계산된 height만 나올 뿐, 어디서 적용되었는지는 나오지 않는 것으로 보아 CSS 내부적으로 저렇게 설정이 되어있다는 것을 알았다.

고민하다가 저 방법을 회피하는 2가지 방법을 알아냈다.

  • 첫 번째 방법 : containing block에 너비와 높이를 설정해서 크기를 정확하게 고정한다.
  • 두 번째 방법 : containing block에 line-height:0 을 적용한다.

둘 중 두 번째 방법을 선택했다.
왜냐하면 containing block의 높이는 video 엘리먼트의 높이에 따라 가변적이기 때문에 임의로 설정하기가 어려웠다.

즉 아래와 같이 설정하면 문제없이 잘 동작한다.

.containing-block{
  width: 100%;
  line-height: 0;
}
video {
  max-width: 100%;
}

슬라이더 구현을 하면서 몇 가지 알게 된 점이 있다.
그중 하나는 max-content 라는 값이다.

css의 width 속성의 값으로 max-content 라는 키워드 값을 사용할 수 있다.

max-content 키워드는 콘텐츠의 최대 너비 또는 높이를 나타낸다.

이는 슬라이더를 구현할 때 유용하게 사용될 수 있다.
사용예시

viewbox는 페이지 내에서 슬라이더가 위치한 레이아웃이다.
viewbox 안에 width: max-content 인 엘리먼트가 overflow된 상태로 포함되어 있다.
따라서 vidwbox에 overflow: hidden 을 설정해야 한다.

슬라이더를 이동하는 기능을 구현하기 위해 Element.scrollLeft 를 사용했다.

이 속성은 숫자값을 설정하면 가로 방향으로 스크롤 할 수 있다.
이 숫자값은 픽셀크기를 나타낸다.
따라서 슬라이더를 이동시키는 기능을 구현하면 픽셀기준으로 왔다갔다 한다.

이미지의 너비 + gap의 너비 만큼 한번에 움직이기 위해서는 이미지 너비와 gap의 너비의 합을 픽셀 단위로 설정하면 된다.

하지만 나는 반응형 웹사이트를 만들기 위해 px단위를 사용하지 않았다.
당연히 이미지의 크기도 rem 기준이다.

px를 rem으로 바꿔주는 코드가 필요했다.

1rem이 16px이니까 나누기 16하면 되는거 아닌가? 싶었지만, 틀렸다.
1rem의 기본 값이 16px일 뿐, 사용자 환경설정에서 얼마든지 기본값을 바꿀 수 있다.

즉, 사용자 환경에서의 1rem값을 px로 변환하는 코드가 필요했다.

여기서 사용한 코드는 다음과 같다.
getComputedStyle(document.documentElement).fontSize

계산된 스타일을 알기 위해 getComputedStyle(엘리먼트) 라는 빌트인 함수를 사용하면 된다.

최상위 document엘리먼트 스타일의 fontSize를 알아내면 기본적으로 적용된 1rem을 알 수 있었다.

참고로 getComputedStyle(엘리먼트) 의 반환값은 단위가 포함된 문자열이다.

CSS 속성명이 카멜케이스로 변경된 것을 확인 할 수 있다.
만약 기존의 CSS 속성명을 기준으로 값을 찾고자 한다면,
getComputedStyle(document.documentElement).getPropertyValue('font-size') 와 같이 사용할 수 있다.

inset 이라는 키워드 값도 알게 되었다.
이 값은 position: absolute 인 엘리먼트에서 사용한다.
top , bottom , left , rigth 의 shorthand 버전이다.

margin , padding 처럼 1, 2, 3, 4개의 값을 가질 수 있으며 4개의 값인 경우 위쪽, 오른쪽, 아래쪽, 왼쪽 으로 설정된다.

overlay{
  inset: 10px 30% 20px 0;
  /*
  top : 10px;
  right: 30%;
  bottom : 20px;
  left : 0;
  */
}

이미지 엘리먼트를 hover할 때 이미지 크기에 맞는 overlay 엘리먼트가 생기도록 구현할 때 유용하게 쓰인다.
overlay예제

스크롤을 이동하며 뷰포트에 엘리먼트가 나타날 때마다 애니메이션이 동작하도록 구현하고자 했다.

애니메이션은 CSS로 구현이 되어있으니 스크롤위치에 따라 해당 엘리먼트에 클래스만 추가 / 제거하면 되는 문제였다.

IntersectionObserver API 라는 훌륭한 WebAPI가 이를 해결해 주었다.

여러 이미지에 애니메이션 효과를 주기 위해서 커스텀 훅을 따로 만들었다.

import { useRef } from "react";

function useScrollAnimation(action) {
  const observer = useRef(
    new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add(action);
        } else {
          entry.target.classList.remove(action);
        }
      });
    })
  );
  const addTarget = (target) => observer.current.observe(target);
  const removeTarget = (target) => observer.current.unobserve(target);

  return { addTarget, removeTarget };
}

export default useScrollAnimation;

이미지가 포함된 컴포넌트에서 useScrollAnimation(애니메이션 동작 클래스 이름) 을 호출한 후에, 이미지 엘리먼트에 ref를 적용하고 addTarget() 의 매개변수로 넘겨주면 해당 엘리먼트가 뷰포트에 나타날때 마다 클래스가 변경된다.

그런데 애니메이션 동작을 여러군데에서 사용하려다 보니 여러 컴포넌트에서 useScrollAnimation() 호출과 useRef 사용이 빈번하게 일어났다.

어차피 이미지에만 애니메이션을 적용할 것이기 때문에 애니메이션이 적용된 이미지를 나타내는 커스텀 컴포넌트를 따로 만들었다.

import React, { useEffect, useRef } from "react";
import "./fadeInImg.css";
import useScrollAnimation from "../../hooks/useScrollAnimation";

const FadeInImg = ({ className, imgUrl }) => {
  const { addTarget, removeTarget } = useScrollAnimation("fade-in");
  const imgRef = useRef(null);
  useEffect(() => {
    if (imgRef.current) {
      addTarget(imgRef.current);
    }
  }, [addTarget]);

  useEffect(() => {
    return () => {
      if (imgRef.current) {
        removeTarget(imgRef.current);
      }
    };
  }, [removeTarget]);
  return (
    <div className={className} ref={imgRef}>
      <img src={imgUrl} alt={imgUrl} />
    </div>
  );
};

export default FadeInImg;

결과물

profile
프론트에_가까운_풀스택_개발자

0개의 댓글