쪼그라드는 웹페이지

최종찬

고객과 직접 마주하는 웹페이지는 다양한 크기의 장치에서 잘 보여야 합니다.
특히나 모바일 게임 회사의 웹페이지는 다양한 크기의 휴대폰에서 잘 보여야 합니다.

괴로운 지점

보통 대응해야되는 화면마다 미디어쿼리를 작성해서 반응형 처리를 합니다. 하지만 그렇게 구현하면 대응하지 않은 중간 크기에서 예상치 못한 디자인 버그가 터지곤 합니다.

가장 많이 겪게되는 디자인 버그는 텍스트가 의도한 공간을 넘어가서 줄이 넘치는 문제죠. 텍스트 내용을 변경해보기도 하고, 미디어쿼리를 추가해서 문제가 생기는 화면 크기에만 스타일을 추가해보고..

웹페이지에 보여줘야 될 내용도 많고 자주 바뀌는데.. 지속적으로 다양한 화면 크기에서 이런 문제를 잡아야 합니다.

안녕하세...요
안녕하세...요

이러자니 QA해야할 것도 많아지고, 어떤 방식으로 고쳐야 할지 디자이너와 일일이 상의해야 하니 커뮤니케이션 비용이 상당히 들어갑니다.


PC 화면이야 대부분의 모니터들이 어떤 적당한 가로폭보다 더 넓을 것이라고 가정할 수 있습니다. 대충 1000px 전후로 가로폭을 잡고 웹페이지를 만든 다음, 웹페이지 내용을 가운데 정렬하여 더 큰 화면에서는 좌우 여백만 늘려주면 됩니다.

좌우 여백만 늘려준 예시
좌우 여백만 늘려준 예시

아이디어

그래서 다음과 같이 작업하기로 합의를 보았습니다.

  • PC 최소 사이즈보다 작은 화면에서는 모바일(태블릿) 화면을 보여준다
  • 태블릿 정도 크기에서는 모바일 화면을 보여주고 좌우에 여백을 준다 (PC 화면처럼)
  • 모바일 화면은 하나만 작업해서 기준 사이즈보다 작은 기기에서는 화면을 쪼그라뜨린다

일반적인 방식은 아니라서 구체적으로 어떻게 작업하겠다는 건지 글로는 설명하기가 어렵더라고요. 저희끼리 내부적으로 커뮤니케이션 하는데도 시간이 좀 걸렸습니다.

이제는 만들어놓은게 있으니 직접 보여드리면 되겠죠!

화면이 줄어들었다 늘어나는 영상

저렇게 만들어 보니 어떤가

지난 1년간 여러 게임사이트들, 회사 사이트, 채용 사이트, 이 블로그까지 대부분의 대외적으로 작업한 웹페이지에서 이 기법을 사용했는데요, 전체 화면을 선형적으로 쪼그라뜨리니 봐야하는 화면 크기의 종류가 확 줄어들었습니다.

브라우저 종류마다 PC 화면, 모바일 화면에서 한번씩 테스트를 거치고 나면, 나중에 특정 모바일 기기의 화면 크기 때문에 들어오는 이슈는 전혀 없었습니다.

다만 스타일 코드를 작성하는 방식에 변경이 좀 있었는데요, 약간의 제약을 감수하면 생산성의 차이는 크게 없었습니다. 아래 기술적인 부분 설명에서 자세히 다루겠습니다.

기술적인 부분

PC 화면에서 태블릿 화면으로 전환하는 부분은 미디어쿼리를 써도 되고 그냥 자바스크립트로 해도 되죠.. 일반적인 반응형 처리는 설명을 생략하고, 쪼그라드는 부분을 작업하기 위해 무슨 고민들을 했고 어떤 방법을 선택했는지 설명하려고 합니다.

예시에 react와 sass(scss) 코드가 사용됩니다. 이 글에서는 이들의 기능에 대해 따로 설명하지 않습니다.

1. zoom 사용하기

MDN zoom 문서: https://mdn.io/zoom

가장 처음에 생각한 방법입니다. 하지만 저희 웹페이지 반응형 처리에는 zoom 속성을 사용하지 못했습니다. 비표준 속성이라 파이어폭스가 지원하지 않거든요.

파이어폭스를 지원하지 않아도 된다면 그냥 이 방법을 사용하면 될 것 같습니다.

const mobileWidth = 640; // 쪼그라들기 시작할 시점
React.useEffect(() => {
  const onResize = () => {
    const zoom = Math.min(window.innerWidth / mobileWidth, 1);
    document.documentElement.style.zoom = `${zoom}`;
  };
  onResize();
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);

documentElementzoom 속성 사용시 주의

zoom 속성을 사용하면 window 객체의 좌표계와 documentElement 이하 좌표계의 스케일이 달라져서 문서 전체의 가로크기를 생각하고 window.innerWidth의 값을 사용하면 예상과 다르게 작동할 수 있습니다.

예를 들어 document.documentElement.style.zoom = '0.5';가 적용되어있다면 문서 안쪽 좌표계는 절반 크기로 축소되어있는 것이기 때문에, 문서 전체의 가로크기를 의미하려면 window.innerWidth * 2를 사용해야 합니다.

js로 화면 전체 크기를 계산하여 사용하는 코드 등에 문제가 생길 수 있습니다. 캐러셀이나 모달 등 전체화면으로 보여줄 일이 많은 UI에서 조심하면 됩니다.

2. transform: scale(...) 사용하기

MDN transform 문서: https://mdn.io/transform

애니메이션이나 가운데 정렬 등의 처리에 엄청 자주 사용되는 transform 속성을 이런 용도로도 활용할 수 있지 않을까 고민해봤는데요.

transform 속성은 적용된 element의 bounding rect에 아무런 영향을 끼치지 않는 문제가 있습니다. 문서 전체에 적용하면 문서 크기만 쪼그라들어 보이고, 브라우저 창의 스크롤 영역은 문서가 줄어들기 전 상태로 보이는 것입니다.

transform scale 적용한 모양
transform scale 적용한 모양

애니메이션에 사용하기엔 유용한 성질이지만 우리와 같은 요구사항에 적용하기는 어렵습니다.

js를 사용해서 컨테이너 크기를 조절하는 방법이 있겠지만 깔끔하게 해결될 거란 기대가 들지 않아서 이 방법은 빠르게 선택지에서 제거하였습니다.

3. rem 사용하기

MDN rem 문서: https://mdn.io/rem
MDN :root 문서: https://mdn.io/root

rem 단위는 문서의 최상위 엘리먼트의 font-size를 나타냅니다.

:root {
  font-size: 30px;
}
.my-component {
  // 30px * 2 = 60px
  font-size: 2rem;
}

저희는 웹페이지를 작업할 때 상대적인 길이(%, em 등)를 제외하면 대부분의 길이에 px 단위(웹에서 px 단위는 실제 픽셀이 아닌, 1/96 인치를 의미합니다.)를 사용하였는데요, 이를 전부 rem 단위로 교체한 다음, 최상위 엘리먼트의 폰트 사이즈를 조정해서 웹페이지 전체를 스케일 시켜보기로 했습니다.

역시 코드를 보여드리는게 빠르겠죠.

$mobile-contents-width: 640px; // 쪼그라들기 시작할 시점

:root {
  font-size: 1px;
  // 640px부터 그 아래로는
  @media (max-width: $mobile-contents-width) {
    // 화면 전체 가로크기(100vw)에서 640을 나눠줍니다.
    font-size: 100vw / ($mobile-contents-width / 1px);
    // 이러면 화면 전체 가로크기가 640px일 때 font-size는 1px이 되고,
    // 화면 전체 가로크기가 320px일 때는 font-size가 0.5px이 됩니다.
  }
}
.my-component {
  // px 단위 대신에 rem 단위를 사용하였습니다.
  width: 123rem;
  // 이러면 쪼그라들기 전에는 123px을 뜻하고,
  // 쪼그라드는 동안에는 최상위 엘리먼트의 font-size가 곱해진 값이 적용됩니다.
  // 예를들어 화면 전체 가로크기가 320px일 때,
  // 123 * 0.5px 해서 61.5px이 되는 것이죠.
}

이걸로 해결됐으면 아주 행복했을 텐데.. 이번에는 웹킷 계열 브라우저가 말썽이었습니다.

사파리와 크롬 등 웹킷계열 브라우저들에는 CJK 등의 로케일에 적용되는 최소 폰트 사이즈라는 개념이 있습니다.
단순하게 생긴 글자 모양을 사용하는 서양에서는 폰트사이즈가 작아도 문서 내용을 읽는데 무리가 없는 반면, 복잡한(특히 한자) 글자 모양을 사용하는 동양에서는 폰트 사이즈가 너무 작으면 글자를 읽을 수가 없다는 것이죠.

브라우저 설정에서 사용자마다 이 최소 폰트 사이즈를 어느정도 조절이 가능하긴 합니다만 어쨌든 기본값은 12px입니다. font-size: 6px라고 작성할 경우 font-size: 12px로 해석됩니다.

rem 단위를 사용할 때에 이 최소 폰트 사이즈 제약에 걸리는 문제가 있었는데요.
font-size에서 rem 단위를 사용할 때에는 rem이 적용된 뒤의 폰트사이즈에서 최소 폰트 사이즈가 적용되지만,
그 외의 css 속성에서 rem 단위를 사용할 때에는 최상위 엘리먼트의 폰트 사이즈에 먼저 최소 폰트 사이즈를 적용한 다음 rem 계산이 이루어집니다.

// $minimum-font-size: 12px;
:root {
  font-size: 1px;
}
.my-component {
  // font-size: 1px * 30 = 30px 👍
  font-size: 30rem;
  // width: max(1px, 12px) * 30 = 360px 🤔❓❓❓
  width: 30rem;
}

이 문제를 피하자면 최상위 엘리먼트의 폰트 사이즈를 한 1000px 정도 잡아놓고 실제로 사용할 때는 0.03rem(1000px * 0.03 = 30px) 같이 작성하는 방법이 있겠지만 너무 비직관적이라 이렇게 쓰고싶지는 않았습니다.

으... 괜찮은 방법인 것 같았는데... 현 시점에서 크롬은 세상을 정복하고 있는 브라우저고, iOS에서는 모든 브라우저가 사파리나 다름없으므로 사파리도 지원을 안할 수 없습니다. 다음 방법으로 넘어갑시다.

4. vw 사용하기

MDN vw 문서: https://mdn.io/vw

화면 크기가 줄어들 때 내용이 줄어들게 하고싶은 것이므로, 화면 가로크기를 나타내는 단위인 vw를 바로 사용해보기로 하였습니다.

앞서 rem을 사용한 접근에서 최상위 엘리먼트의 폰트사이즈에 이미 vw 단위를 사용했었죠. 각각 엘리먼트의 속성에다가 바로 vw 단위를 사용하면 최소 폰트 사이즈 문제를 피할 수 있을 것입니다.

$mobile-contents-width: 640px; // 쪼그라들기 시작할 시점

.my-component {
  // 화면 크기가 640px보다 큰 경우엔 30px 사용
  width: 30px;
  // 640px 이전 화면크기 까지는 vw 단위 사용
  @media (max-width: $mobile-contents-width) {
    width: 30px / $mobile-contents-width * 100vw;
  }
}

잘 작동합니다만, 모든 속성을 일일이 저렇게 쓰면 코드가 너무 번잡해지겠죠.

sass mixin으로 묶어보았습니다.

$mobile-contents-width: 640px; // 쪼그라들기 시작할 시점
@mixin scale($property, $px) {
  // 화면 크기가 640px보다 큰 경우엔 30px 사용
  #{$property}: $px;
  // 640px 이전 화면크기 까지는 vw 단위 사용
  @media (max-width: $mobile-contents-width) {
    #{$property}: $px / $mobile-contents-width * 100vw;
  }
}

.my-component {
  @include scale(width, 30px);
}

이제 속성: 값 방식으로 작성하던 css 코드를 @include scale(속성, 값)으로 전부 바꿔 적으면 쪼그라드는 웹페이지가 됩니다!

그런데 속성: 값 값 값 값 방식이 나타난다면?

margin: 1px 2px 3px 4px 이렇게 값이 한번에 여러개 들어있는 경우는 위 mixin으로 처리가 안되죠.
border: 1px solid black 같이 숫자가 아닌게 섞여있는 경우도 있습니다.

sass의 기능을 좀 더 활용해서 그런 경우를 처리해보았습니다.

$mobile-contents-width: 640px; // 쪼그라들기 시작할 시점
@mixin scale($property, $px) {

  $w: 1px / $mobile-contents-width * 100;

  $pxs: null;
  $vws: null;
  // 여러 값에 대한 처리로 일반화
  @each $v in $px {
    // border: 1px solid black 같은 코드에서 숫자(1px)만 처리
    @if type-of($v) == 'number' {
      $px: $v / 1px;
      $pxs: append($pxs, if($px == 0, 0, $px + px));
      $vw: $px * $w;
      $vws: append($vws, if($vw == 0, 0, $vw + vw));
    } @else {
      $pxs: append($pxs, $v);
      $vws: append($vws, $v);
    }
  }
  #{$property}: $pxs;
  @media (max-width: $mobile-contents-width) {
    #{$property}: $vws;
  }
}

.my-component {
  @include scale(width, 30px); // 전부
  @include scale(margin, 1px 2px 3px 4px); // 잘
  @include scale(border, 1px solid black); // 처리됩니다
}

빌드된 css 파일에 미디어 쿼리가 너무 많이 보여요

sass는 기본적으로 서로 떨어져있는 media query를 합쳐주지 않습니다.

.my-component {
  @include scale(width, 640px);
  @include scale(height, 640px);
}

위 코드를 SASS 놀이터 등에서 돌려보면 다음과 같이 변환됩니다.

.my-component {
  width: 640px;
  height: 640px;
}
@media (max-width: 640px) {
  .my-component {
    width: 100vw;
  }
}
@media (max-width: 640px) {
  .my-component {
    height: 100vw;
  }
}

미디어 쿼리를 하나로 합쳐줄 법도 한데 속성별로 떨어져있는 것을 확인할 수 있습니다.

css 최적화 도구인 CSSO에는 같은 규칙의 미디어 쿼리가 서로 떨어져있으면 병합시켜주는 기능이 있는데요.

CSSO 놀이터에서 위 결과물을 넣고 돌려보면 다음과 같이 병합되는 것을 확인할 수 있습니다.

.my-component {
  width: 640px;
  height: 640px
}
@media (max-width:640px) {
  .my-component {
    width: 100vw;
    height: 100vw
  }
}

기본적으로는 안전하게 합쳐지는 것이 보장되는 경우에만 합쳐줍니다만, forceMediaMerge 옵션을 사용해서 좀 더 공격적인 최적화를 할 수도 있습니다.

caveat

사실 다 해결된 건 아니고 한가지 해결하지 못한 문제가 있습니다.

앞서 설명한 mixin은 transform, filter 등 추가 문법이 있는 속성을 처리하지 못하는데요, 예를 들면 transform: translate(1px, 2px)와 같은 코드를 처리할 수 없습니다.

transform 문법을 따로 파싱해야 하는데, sass 코드만으로는 처리하기 까다로웠습니다.
커스텀 함수와 같은 실험 확장 기능을 사용하면 해결할 수 있을 듯 한데, 지금까지는 적당히 우회 처리를 해주는 것으로 충분했기에 따로 시간을 들여 작업하지 않았습니다.

One more thing...

처음에 빠르게 버려진 zoom 속성은 의외의 쓸모가 있었습니다.

웹페이지를 세로 화면으로 볼 것을 가정하고 만들다보니 가로화면에서는 중요한 정보들이 한 눈에 들어오지 않는 문제가 있었는데요.

저희는 간단하게 zoom: 0.5를 주어서 가로화면에서도 적당히 잘 보이도록 만들었습니다.
이 트릭은 파이어폭스에서는 작동하지 않습니다만 애초에 사소한 개선이라 크게 신경쓰지 않았습니다.

$pc-contents-width: 1000px; // pc 화면이 시작되는 시점
:root {
  // 모바일 화면이면서 가로화면일 경우 내용 크기를 줄여줍니다.
  @media (max-width: $pc-contents-width - 1px) and (orientation: landscape) {
    zoom: 0.5; // 0.5 ~ 0.6 정도가 적당한 것 같습니다.
  }
}

zoom 적용한 모양
zoom 적용한 모양

© 2024 Devsisters Corp. All Rights Reserved.