저는 이전에 VR 솔루션을 서비스로 제공하는 스타트업에서 프론트엔드 개발자로 근무한 경험이 있습니다.
그때 제가 담당한 주요 프로젝트 중 하나가 버전 업그레이드이었습니다. 그 내용은 기존에 Angular와 PHP 기반의 웹 어플리케이션을 Vue와 Node.js로 이동하는 것이었습니다. 무겁고, 레거시 코드가 많아서 유지보수가 까다로웠기 때문입니다.
오늘의 포스트는 그 과정 중에서 360 파노라마 뷰어(Viewer)를 재설계했던 과정을 소개하고자 합니다. 사실 어떻게 보면 간단한 내용일 수도 있으나, 제가 프로젝트를 하면서 가장 흥미를 느꼈기도 하고, 애착이 가는 부분이라 꼭 한 번쯤은 포스트로 기록을 남겨 보고 싶었습니다.
이 포스트의 내용은 회사 업무의 일환이기도 하기 때문에, 자세한 코드보다는 주로 리팩토링할 때 추구했던 설계의 관점에 대해서 언급하도록 하겠습니다.
프로젝트 소개
일단 완성된 결과물부터 소개하자면, 아래의 이미지를 참고해주세요.
이 프로젝트는 360 파노라마 사진을 이용해서 마치 로드뷰처럼 사진들을 연결하고, 그것을 결과물로 볼 수 있는 뷰어(Viewer)입니다.
사용된 라이브러리는 회사 측에서 개발한 것으로 알고 있고, 저는 사용자의 액션과 라이브러리 간의 이벤트를 연결하고 반응형 디자인과 UI 컴포넌트 등을 개발했습니다.
일단 오늘 주제는 반응형 레이아웃 설계 니까, 지금부터는 그 부분에 대해서 좀 더 집중해보겠습니다.
기존 프로젝트의 문제점
말 그대로 굴러만 가는 코드
기존에 Angular로 작성된 프로젝트에서는 뷰어가 컴포넌트 별로 관리되고 있지 않았습니다. 그래서 뷰어의 모든 스타일을 담당하고 있는 main.scss
파일만 거의 4000줄(…)에 달하는, 엄청나게 장황하고 긴 코드로 관리가 되고 있었습니다.
게다가 많은 사람의 손을 탄 코드라 그런지, 반응형 레이아웃도 뒤죽박죽이었습니다.
@media (max-width: 1850px) { ... }
@media (min-width: 1000px) and (max-height: 626px) { ... }
@media (max-width: 336px) { ... }
@media (max-width: 850px) and (max-height: 500px) { ... }
대충 이런 느낌이었습니다.
“저 숫자는 도대체 어떤 기준으로 정해진 것인가..?”
“순서는 왜 저렇게 뒤죽박죽으로 작성되어 있는거지?”
알 수 없는 기준으로 나눠진 분기점(breakpoint) 에 알 수 없는 기준으로 나눠진 스타일들이 있었습니다. 중구난방 그 자체였습니다.
추측컨대 새로운 요구 사항이 들어올 때, 누군가 당장 필요한 부분에 대해서만 추가적으로 스타일을 추가한 것으로 보입니다. 아무래도 명시적인 스타일 가이드가 없기도 했고, 한 군데에서 관리를 할 필요성을 못 느껴서 그런 듯 하였습니다.
그래서 원본으로 참고했다던 디자인을 살펴보았습니다. 하지만 디자인 역시 설명이 많이 부족해보였는데, 데스크탑과 모바일 버전의 UI가 담긴 사진 2장이 전부였습니다. 그리고 이미 어플리케이션에는 새 기능이 많이 탑재된 상태라, 이미 구식의(outdated) 디자인이었습니다.
이러다보니 새로운 기획이 반영되는 데에도 시간이 오래 걸리는 데다가 종종 의도하지 않은 사이드 이펙트가 발생하게 되었고, 결과적으로 유지보수가 매우 어려웠습니다. 특히나 당시의 저는 입사한 지 얼마되지 않은 주니어여서, 코드의 전체 구조를 잘 모르는 상태라 고통이 더욱 컸습니다.
1차 리팩토링
극대노
결국 저는 이 부분이 장기적인 관점에서 리팩토링이 필요할 것 같다고 팀원들에게 얘기했고, 리팩토링 태스크를 진행하게 되었습니다.
개발 전에 미리 고려한 점
- 스크린의 높이 요소가 반응적이어야 한다
일반적으로 반응형 레이아웃을 생각하면 스크린의 width
의 길이는 유한하되 height
의 길이는 무한히 늘릴 수 있어서, 가로로 넘치는 컨텐츠를 세로 방향으로 배치해 스크롤 할 수 있다는 것을 원칙으로 하고 있습니다.
하지만 지금 개발 중인 반응형 레이아웃은 너비와 높이가 스크린과 일치하는, 360 파노라마 뷰어 위에서만 동작하는 UI 컴포넌트라는 특성이 있었습니다. 즉, 스크린의 높이 요소 역시 반응형 레이아웃에서 고려해야 했습니다.
- 어떠한 모양의 스크린 레이아웃도 커버할 수 있어야 한다
그리고 뷰어 특성 상 사용자가 어떤 디바이스에서 접근할지 모르기도 하고, <iframe>
태그를 이용해 다른 웹 사이트에서 임베디드(embeded)되기도 하기 때문에, 극단적인 환경의 스크린 레이아웃 역시 고려해야 했습니다.
나의 아이디어
우선 갈갈이 찢겨진 분기점들을 한 군데에서 모아 관리하기로 했습니다.
$width: 830px;
$height: 830px;
그리고 분기점을 바탕으로, 스크린의 width
와 height
값을 커버할 수 있는 몇 개의 레이아웃으로 나눕니다.
지금 너무 많은 분기점들을 이렇게 4개로만 구분하려고 하는데, 괜찮지 않을까?
저는 위와 같이, 좌상단을 원점으로 830px
의 정사각형을 분기점으로 잡은 후, 이를 기준으로 4개 레이아웃을 나눠 각각에 대한 반응형 레이아웃을 관리하면 어떨까라는 생각을 했습니다.
왜 하필 830px
이냐고 물으시는 분이 있다면, 스크린의 높이가 스마트폰 중에서 가장 긴 iPhone X의 뷰포트(viewport) 높이가 812px
이어서 이를 커버하기 위해 설정한 값입니다.
아무튼 저는 width
가 830px
이상이거나 미만인 경우와 함께 height
가 830px
이상이거나 미만인 경우로 케이스를 나누어 생각하기로 했습니다.
- Case 1: 정사각형보다 작은 모양
- Case 2: 가로로 긴 직사각형 모양
- Case 3: 세로로 긴 직사각형 모양
- Case 4: 위의 것들보다 큰 모양
당시에 보았을 때는 꽤 괜찮은 생각이라고 생각했고(착각이었음), 스타일을 아래와 같이 나누었습니다. min
과 max
가 다르다는 것에 주의해주세요.
// Case 1: 정사각형보다 작은 모양
@media (max-width: $width) and (max-height: $height) { ... }
// Case 2: 가로로 긴 직사각형 모양
@media (min-width: $width) and (max-height: $height) { ... }
// Case 3: 세로로 긴 직사각형 모양
@media (max-width: $width) and (min-height: $height) { ... }
// Case 4: 위의 것들보다 큰 모양
@media (min-width: $width) and (min-height: $height) { ... }
워낙 원본 코드가 복잡해서 위의 기준대로 100% 나누지는 못했지만, 중복되는 코드는 위쪽으로 빼버리고 반응형에만 입혀지는 코드들을 분리하면서 대부분의 코드를 위의 기준대로 나눌 수 있었습니다.
한계
하지만 태블릿 레이아웃이 추가되면서 문제가 찾아왔습니다. 다른 추가적인 케이스는 전혀 고려하지 않았던 탓에, 기존의 4개로 나누었던 레이아웃으로 커버가 불가능해졌습니다.
게다가 제가 큰 착각을 하고 있었는데, Case 1, Case 2, Case 3의 경우에는 스크린의 width
나 height
중 하나가 830px
보다 작기 때문에 당연히 모바일 디바이스에서만 접근한다는 생각을 했었습니다. 하지만 업데이트 후, 웹에서 <iframe>
태그로 크기가 작은 뷰어를 보게 되면 레이아웃이 깨진다는 버그 리포트가 들어왔습니다. 예를 들자면 모바일 디바이스에서는 보여지면 안 되는 버튼들이 보이는 것처럼요.
저의 잘못된 설계와 함께 코드 퀄리티에 쏟을 시간이 모자랐던 탓 때문에, 그리고 저기에다가 임시방편으로 특정 컨디션을 위한 변수를 계속 추가하면서…
$width_collapse: 760px;
$width_normal: 830px;
$height_collapse: 760px;
$height_normal: 830px;
$height_short: 570px;
$width_short: 375px;
$iframe_small_width: 400px;
$iframe_small_height: 400px;
결국에는 설명 없이는 이해하기 어려운 난해한 코드가 나오고 말았습니다. 리팩토링 전에는 레이아웃이 각자 따로 놀고 있었다면, 지금은 관리해야 할 레이아웃 상태가 너무 많아져 버렸습니다.
2차 리팩토링
레거시 코드는 버리고, 이제 제대로 시작할 때가 왔다!
그러다가 Angular로 작성된 프로젝트를 Vue로 버전 업 하는 프로젝트가 시작되었습니다. 이제는 레거시 코드를 베이스로 작성하는 게 아닌, 처음부터 제가 레이아웃 디자인과 관련된 모든 것을 설계하고 작성할 기회가 생겼습니다.
개발 전에 미리 고려한 점
두 번째로 만들게 된 레이아웃 설계인만큼, 같은 실수는 반복하지 않겠다는 마음으로 충분한 설계 시간을 가졌습니다.
- 반응형 디자인은 디바이스의 종류에 종속적이어서는 안 된다
가장 기초적인 부분인데, 이 부분을 간과했었습니다. 화면의 너비와 관련 없는 UI 부분은 독립적으로 상태를 관리하는 코드로 분리했습니다.
- 모든 디바이스를 커버할 수 있는 레이아웃이면서 분기점을 최소화한다
저번의 실수를 바탕으로 분기점이 많으면, 오히려 없는 것만 못하다는 것을 깨달았습니다. 따라서 가능한 적은 수의 분기점을 사용하도록 합니다.
나의 두 번째 아이디어
별 거 아닌데, 실패한 경험이 있어서 그런지 오기가 생겼습니다. 종이에 이것 저것 그려보며 설계를 하다가 뭔가 감이 잡혔습니다.
”극단적인 환경에서는 무조건 모바일 레이아웃으로 보여지게”
”그 외의 환경에서는이를 화면 높이에 따라 구분할 수 있게”
분기점을 바탕으로 레이아웃을 나누지 말고, 레이아웃 모양대로 분기점을 나누자!
첫 번째 리팩토링에서는 분기점을 바탕으로 레이아웃을 나누다보니, 새로운 레이아웃을 추가하거나 관리하기가 유동적이지 않았습니다. 하지만 우리가 애초부터 다루고 있었던 문제의 본질은 다른 종류의 레이아웃을 만드는 것이고, 분기점을 나누는 것은 이를 달성하기 위한 과정 중 하나였습니다.
- Case 1: 데스크탑 레이아웃(큰 툴바, 큰 리스트)
- Case 2: 태블릿 레이아웃(큰 툴바, 작은 리스트)
- Case 3: 모바일 레이아웃(작은 툴바, 큰 리스트)
위의 조건을 코드로 나타내보면 아래와 같습니다.
$width: 760px;
$tablet-height: 590px;
$desktop-height: 760px;
// Case 1: 데스크탑 레이아웃
// media query 없이 기본 스타일로 작성
// Case 2: 태블릿 레이아웃
@media (min-height: $tablet-height) and (max-height: $desktop-height) and (min-width: $width) { ... }
// Case 3: 모바일 레이아웃
@media (max-width: $width), (max-height: $tablet-height) { ... }
우선은 데스크탑 스타일의 디자인을 먼저 작성한 후, 만약 스크린의 width
나 height
가 충분히 작아서 태블릿 또는 모바일 레이아웃 디자인이 적용되어야 한다면 미디어 쿼리(media query)가 이를 오버라이딩하는 방식을 적용했습니다.
그리고 각 컴포넌트 별로 파일을 나누어서, 유지보수를 쉽게 하고 재사용성을 높였습니다.
결론
반응형 최고!
뭐 어쨌거나, 다양한 디바이스 환경에서도 잘 동작하는 뷰어를 만들었다는 게 결론입니다.
개인적으로는 기존에 레거시 코드로 작성되었던 구조를 더 나은 구조로 개선시켰다는 점에서 의의가 있다고 생각합니다. 물론 실패를 한 번 경험했기 때문에 개인적인 애착이 가는 것 같기도 하고요.
사실 이 작업은 약 1년 전 쯤에 끝난 일이라… 지금보다 더 주니어(?)였을 때 작성한 코드입니다. 그 당시에도 한 번 기록으로 남겨보고 싶었던 경험이었는데, 게으른 탓인지 이제와서야 포스팅을 하네요. 기억이 잘 안나서 혼났습니다.
일반적인 반응형 레이아웃과는 조금 형태가 다르지만, 제 경험이 반응형 레이아웃을 공부하고 계신 분들께 도움이 되길 바라면서 글을 마치도록 하겠습니다.
포스트를 작성하다 궁금해서 검색해보니, 이 곳에서는 iPhone XS 높이가
812px
이고 iPhone XR은896px
까지 된다고 하는군요.