프로젝트/하루한냥

[리팩토링] 처음 만난 모바일, 효도폰 어플을 만든 사연

알파카털파카 2023. 12. 7. 06:39
[리팩토링]
처음 만난 모바일, 효도폰 어플을 만든 사연

 

 

 

열심히 디자인하고 개발한 프로젝트에 스타일링 문제가 있을 거라곤 생각지 못했다. 모바일 화면을 직접 보기 전까지는. 이번 포스트는 모바일 화면에 맞춰 프로젝트를 진행할 때, 실제 기기로 디버깅하는 것이 중요하다는 깨달음의 과정을 담고 있다. 컴퓨터 브라우저에서는 정상적으로 동작해도, 모바일 기기로 테스트해보면 이상하게 돌아가는 버그도 있었다. 동일한 프로젝트를 실행할 때 데스크탑과 모바일 환경에서 어떤 차이가 있는지 알아보고, 리팩토링하는 과정을 적어본다. 

 

 


 

 

문제 1. 실제 기기와 개발 환경에서의 화면 차이 

모바일 최적화된 애플리케이션을 만들고자 했으면서, 실제 기기로 테스트를 해볼 생각은 뒤늦게 했다. 프로젝트가 마무리된 지금에서야 확인한 것은 아니고, 기능을 얼추 다 구현해놓은 타이밍에 실제 스마트폰으로 확인했다. 그 말은, CSS 스타일링이 다 끝나있었다는 얘기.

 

긴 설명 대신 스타일링 보완 전후 이미지를 첨부한다. 왼쪽(⬅️)이 Before, 오른쪽(➡️)이 After 화면이다. 어떤 부분이 달라졌는지 비교하는 재미가 있다.

 

 

로그인 화면

 

일기 작성 화면

 

타임라인 화면

 

검사 화면

 

검사 결과 화면

 

 

나는 갤럭시 S20을 사용하고 있다. 내 기기에서 이 프로젝트를 처음 실행해보고 깜짝 놀랐다. 너무 못생겨서.

 

로그인 화면부터 압도적인 효도폰 냄새가 난다. 하루한냥은 개인적으로 진행하는 프로젝트이기 때문에 명확한 유저 타겟을 설정해놓진 않았지만, 이러한 감정일기는 대략 2030 또래가 많이 사용하기 때문에 일부러 요소를 큼직하게 만들 필요는 없다. 분명 컴퓨터 브라우저에서는 괜찮아 보였다. Device Mode로 꼼꼼하게 확인하면서 작업했다. 그 와중에 갤럭시 폴드 사이즈까지 대응해 두었다. 그래서 조금 억울했다. 🤦🏻‍♀️

 

이전 화면의 공통점으로는 요소들의 여백이 상당히 넓다는 점이 있다. 나는 일반적으로 사용되는 헤더나 메뉴바의 height 사이즈, 본문의 padding 등을 잘 몰라서 내가 보기에 예쁜 화면으로 디자인했었다. 주위에서 여백이 좀 과하지 않냐는 피드백도 받았었는데, 내 마음대로 하겠다고 밀고 나가다가 직접 폰으로 확인하고 나니까 민망했다. 많은 사람들이 사용하는 것에는 다 이유가 있다.

 

 

 

해결방법 : 스타일 대공사 착수

1. 헤더, 메뉴바 height 줄이기

큼직하게 보이는 요소부터 축소 작업에 돌입했다. 현업에서는 헤더 사이즈로 height: 60px을 주로 사용한다고 하는데, 갑자기 확 줄어들면 섭섭하므로 64px로 맞췄다. 난 동양인답게 여백의 미를 좋아하나보다. 메뉴바도 10px 줄였다. 스타일에 관한 데이터는 styleToken.css.ts 파일로 분리해 두었다. 

 

const SIZE_PROPERTIES = {
  headerHeight: `64px`,
  menuHeight: `70px`,
};

const styleToken = {
  color: COLOR_PROPERTIES,
  font: FONT_PROPERTIES,
  size: SIZE_PROPERTIES,
};

export default styleToken;

 

헤더를 수정하면서 뒤로가기 버튼이 png로 되어있는 것을 발견하고 svg 파일로 교체했다. 어쩐지 화살표 화질이 좋지 않았다. png는 비트맵 파일 확장자이기 때문에 확대 시 깨지는 반면, svg는 벡터 파일 형식이므로 확대해도 깨지지 않는다. 가시성을 높이기 위해 색상도 진하게 처리했다. 개인적으로는 연한 회색이 더 마음에 들지만, 사용성으로 봤을 때는 진한 색이 눈에 확 띄어서 편리하다.

 

뒤로가기 아이콘 변경 전, 후 코드

 

뒤로가기 아이콘 변경 전, 후 헤더

 

 

2. HTML 요소 크기 축소 - Input, Button

가장 자극적으로 못생긴 요소는 인풋과 버튼이다. 효도폰 같아보이는 데에 일등공신이다. 작업하면서 변화 체감이 많이 되는 부분이기도 했다. 다행히 몇 가지 요소를 공통 컴포넌트로 분리해두어서, 모든 input, button 요소를 일일이 찾아 바꾸지 않아도 되었다.

 

예시로 BaseButton 컴포넌트를 들고왔다. 각 버튼의 성격마다 색상이 달라야 하기 때문에 컬러 스키마를 지정하고, props를 넘겨받아 기능하도록 만들었다. 

 

import { ButtonHTMLAttributes, PropsWithChildren } from 'react';
import styled from '@emotion/styled';
import { styleToken } from '@ui/styles';

const colorSchemaStyle = {
  // 생략
};

type ColorSchema = 'primary' | 'success' | 'danger' | 'info' | 'light' | 'disabled';

type Props = {
  colorTheme: ColorSchema;
  width?: string;
  height?: string;
  minHeight?: string;
  onKeyPress?: (e: KeyboardEvent) => void;
} & ButtonHTMLAttributes<HTMLButtonElement>;

export function BaseButton({ children, colorTheme, onKeyPress, ...props }: PropsWithChildren<Props>) {
  return (
    <Button type="button" colorTheme={colorTheme} onKeyPress={onKeyPress} {...props}>
      {children}
    </Button>
  );
}

const Button = styled.button<Props>`
  color: ${(props) => colorSchemaStyle[props.colorTheme].mainColor};
  background-color: ${(props) => colorSchemaStyle[props.colorTheme].backgroundColor};
  width: ${(props) => props.width || '100%'};
  height: ${(props) => props.height || '54px'};
  min-height: ${(props) => props.minHeight || '0'};
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  font-weight: 600;
  border: none;
  border-radius: 14px;
  font-size: 16px;
  cursor: pointer;

  &:hover {
    background-color: ${(props) => colorSchemaStyle[props.colorTheme].backgroundColor}dd;
  }
  
  &:disabled {
    cursor: not-allowed;
    background-color: ${styleToken.color.gray5};
    color: ${styleToken.color.white};
  }
`;

 

변경 전, 변경 후

 

인풋과 버튼의 height를 68px에서 54px로 줄였다. 요소에 적용된 margin도 전체적으로 줄여주었다. 어렵진 않지만 자잘한 작업량이 많아서, 피그마의 디자인 시안까지 이렇게 변화된 사이즈로 바꿔두진 못했다. 다음에 프로젝트를 구상할 때 이번 경험이 도움이 될 것 같다. 

 

 

3. padding 줄이기

각 요소의 Container 마다, 그리고 본문 Body 영역에 이중으로 padding이 많이 들어가 있었다. padding이 과하면 콘텐츠가 보여지는 화면이 작아져 답답하게 느껴진다. 양 옆으로 눌려있는 것처럼 보인다. 생각보다 패딩을 적게 줘도 사용하는데는 전혀 문제가 없었다. 특히 스크린샷의 오른쪽 이미지와 같이, 문장이 이상한 곳에서 끊겨 보이는 부분은 개선이 필요했다. 해당 컨테이너와 Body의 패딩을 줄여서 개선한 결과, width 340px까지는 문장 끊어읽기가 정상적으로 유지되도록 했다. 

 

전체 화면 스크린샷은 글의 앞부분에 실린 이미지를 참고하면 된다.

 

Before - padding이 과도하게 적용된 모습

 

After - padding을 줄인 모습

 

 

3-1. 다른 사이트의 경우는 어떨까?

다른 사이트에서는 모바일 화면의 패딩이 얼마나 되는지 찾아보았다. 네이버, 구글의 경우 본문에 margin/padding이 아예 들어가있지 않은 페이지도 있었으며, 특정 페이지에서는 20px 만큼의 여백이 있기도 했다. 

 

 

(참고1) 네이버, 구글 모바일 화면

 

(참고2) 다음 스토리 페이지의 margin

 

(참고3) 네이버 날씨 페이지의 padding

 

작은 모바일 화면에서 가능한 많은 정보를 담아 유저에게 보여주기 위함인가보다. 평소 사용할 때는 눈에 들어오지 않았는데, 관심을 가지고 찾아보니 재미있었다. 여백이 아예 없어도 왠지 낯설지 않고 눈에 익은걸 보면 다른 사이트도 이런식으로 많이들 사용하는 듯 하다. 

 

 

 

 

문제 2. 모바일 100vh 스크롤 이슈 

데스크탑 개발자 도구 Device Mode에서는 발견되지 않은 버그다. 모바일로 접속하면 콘텐츠를 넘어서는 부분까지 스크롤이 가능한 현상이 발생했다. 화면 전체 레이아웃을 구현해둔 src/ui/components/layout/Page.tsx 파일에서 해당 문제의 원인을 찾을 수 있었다. 

 

100vh 이슈가 발생한 화면

 

기존 코드를 살펴보자. height: 100vh를 지정해 두었다. 뷰포트 단위인 vh와 백분율을 의미하는 100을 이용한 스타일링이다. 100vh는 상단 url을 입력하는 주소창 영역과, 하단 네비게이션 영역의 사이즈를 포함하기 때문에 이런 문제가 발생한다. 100vh 이슈는 예전에 학습해 두어서 문제가 어디에서 발생했는지 금방 찾을 수 있었다. 인덱싱의 중요성을 체험하고 있다.

 

// 기존 코드
export function Page({ children }: PropsWithChildren) {
  return <Container>{children}</Container>;
}

const Container = styled.div`
  width: 100vw;
  height: 100vh; 📌
  background-color: #e9e9e9;
`;

 

 

 

해결방법 : innerHeight 값 이용하기 

이를 해결하기 위해 innerHeight를 이용했다. window의 innerHeight 속성은 창의 레이아웃 뷰포트 높이를 픽셀로 나타낸 값이다. 여기에 0.01을 곱해 vh 단위값을 얻는다. setProperty로 새로운 CSS 속성을 설정한다. 

 

// 리팩토링 후 코드
import styled from '@emotion/styled';
import { styleToken } from '@ui/styles';
import { PropsWithChildren, useEffect } from 'react';

export function Page({ children }: PropsWithChildren) {
  function setScreenSize() {
    const vh = window.innerHeight * 0.01;

    document.documentElement.style.setProperty('--vh', `${vh}px`);
  }

  useEffect(() => {
    setScreenSize();
  }, []);

  return <Container>{children}</Container>;
}

const Container = styled.div`
  width: 100vw;
  height: calc(var(--vh, 1vh) * 100); ✅
  background-color: ${styleToken.color.gray5};
`;

 

이해를 돕기 위해 예시를 들어본다. window.innerHeight로 구한 높이가 978 픽셀이라고 했을 때, 0.01을 곱했으므로 const vh에 담긴 값은 9.78이다. 이 값으로 문서의 루트 요소를 나타내는 엘리먼트의 스타일 속성을 새로 지정한다. CSS-in-JS 방식으로 스타일링할 때, 위에서 계산된 vh의 값을 가져와서 100을 곱하면 height에 978 픽셀이 지정된다. var 함수의 두번째 인수로는 1vh를 주었고, 이는 속성이 유효하지 않거나 --vh에 값이 할당되지 않았을 경우를 대비한 기본값이다. 이렇게하면 스크롤이 콘텐츠를 초과해 발생하는 문제를 해결할 수 있다. 

 

 

 

 

문제 3. 모바일과 데스크탑의 hover 설정 차이 

이번 이슈는 이런저런 기능을 테스트하다가 발견됐다. 일기 작성 페이지에서 감정을 고를 때 생긴 문제다. 흐름은 이렇다.

 

1. 감정 아이콘에 마우스 hover 시 opacity를 100%로 설정해두었다.

2. 클릭하면 감정이 선택되고, 한번 더 클릭하면 선택이 취소된다. 

 

데스크탑에서 Device Mode를 사용해도 직접 터치를 해볼 수는 없다. 막상 스마트폰으로 터치해보니 hover된 기록이 계속 남아있는지, 선택을 취소해도 opacity값이 100%로 되어있었다. 다른 빈 곳을 터치해주어야 다시 기본값인 45%로 돌아갔다. 이 문제를 해결하려고 CSS에 이리저리 다양한 시도를 해봤는데 잘 안됐고, 그러다 미디어 쿼리로 해결하는 방법을 발견했다. 

 

(좌) 선택하지 않았을 때, (우) 선택했을 때

 

 

 

해결방법 : 미디어 쿼리로 터치 가능 디바이스 구분하기

미디어 쿼리의 hover 특성을 이용하면, 사용자가 요소 위에 hover할 수 있는 경우에 스타일을 적용할 수 있다. 

 

@media (hover: hover) 는 호버 가능한 디바이스를 타겟팅할 수 있다. PC가 여기에 포함된다. 이때는 대개 터치 기반 기기(스마트폰, 태블릿 등)에서는 작동하지 않는다. 

@media (hover: none) 은 호버 불가능한 디바이스를 타겟팅한다. 터치 기반 디바이스에 스타일이 적용된다. 

 

const EmotionHeader = styled.div`
  /* 호버 가능한 디바이스에서의 스타일 */
  @media (hover: hover) {
    &:hover {
      opacity: 1;
    }
  }

  /* 호버 불가능한 디바이스에서의 스타일 */
  @media (hover: none) {
    /* 터치 기반 디바이스에서의 스타일 설정 */
  }
`;

 

나의 경우, PC에서만 hover 처리를 하고 싶기 때문에, 미디어 쿼리로 hover 가능한 디바이스에만 해당 CSS를 적용했다. 방법을 알게 되니 간단하게 해결되는 이슈였다. 

 

@media (hover: hover) {
  &:hover {
    opacity: 1;
  }
}

 

 

 

 

내용 요약과 리팩토링 결과

1. 실제 기기로 디버깅을 직접 꼭 해보자.
2. 모바일 화면에서는 여백이 적어도 콘텐츠를 보여주는데 지장이 없다.
3. 모바일 100vh 스크롤 초과 이슈는 innerHeight 속성을 이용해 해결할 수 있다.
4. 모바일과 데스크탑 각각에 다른 CSS를 적용하려면 미디어 쿼리hover 특성을 사용하자.

 

 

리팩토링 후 아이폰 테스트 영상

 

 

 

마치며

노트북으로 작업한 프로젝트가 모바일로 옮겨갔을 때 어떻게 보이는지, 어떤 부분을 신경써야 하는지 배울 수 있는 경험이었다. 실제 기기로 테스트해보지 않으면 나도 모르게 효도폰ver. 프로젝트를 만들게 될 수 있다. 특히 100vh 이슈는 이론으로만 보다가 이렇게 실전에서 만나보니 반가울 지경이었다. 글로 풀어쓰면서 더 학습이 되었다. 컴퓨터 화면은 모바일에 비해 상대적으로 커다랗기 때문에, 여백이 많아도 그럭저럭 괜찮아 보인다. 동일한 화면 구성이 작은 스마트폰으로 보았을 때는 과하게 느껴지는걸 보면 일종의 착시같기도 하다. 하루한냥 프로젝트를 진행하며 쓰고 싶었던 글감은 거의 다 풀어냈다. 정말 마지막으로, 프로젝트를 다 끝내고 최종_회고 만을 남겨두고 있다.