동적 그리드 리스트(Dynamic Grid List) UI
이번 프로젝트를 진행하면서 pinterest.com 스타일의 동적 그리드 리스트를 모바일에 적용하기 위한 모듈을 만들었다. 타일 리스트(Tile List), 동적 그리드 뷰(Dynamic Grid View) 등.. 다양한 이름으로 불리는 UI인데 공식적인 명칭이 무엇인지는 잘 모르겠다. 프로젝트를 진행하면서 명칭을 통일해야 할 것 같아서, 임의로 동적 그리그 리스트라는 이름으로 부르기로 했다(dynamic grid list로 검색했을 때 가장 많은 자료를 찾을 수 있었기에).
이전에 한창 이런 형태의 UI가 유행하던 시절이 있었는데, 당시에 핀터레스트와 유사한 형태의 서비스를 개발했던 적이 있었던터라 이번에도 별 어려움 없이 만들 수 있을 거라고 생각했다(당시에는 데스크톱 버전).
이 UI는 이미지의 사이즈에 따라서 그리드의 height가 동적으로 변하는 것이 특징이다. CSS-Only Pinterest Style Columns Layout처럼 CSS3의 column-count, column-gap, column-fill 속성을 이용하면 아주 간단하게 구현할 수 있는데 언제나 문제는 하위 단말기 지원 여부다.
단말기 호환성 문제 때문에 CSS3를 사용할 수 없다면, 결국 JavaScript로 해결해야 한다.
조금만 검색해보면 동적 그리드 리스트를 구현한 많은 오픈 소스 라이브러를 찾을 수 있다. 모든 코드를 살펴보지는 못했지만, 대게 그리드에 position:absolute 속성을 준 다음에, 각각의 left, top 값을 계산해서 해당 위치에 그리드를 배치하는 방식으로 처리하고 있는 것 같다.
간단히 계산하는 방법을 정리해보면,
- 우선 전체 화면 width을 화면에 나타내고 싶은 열의 개수(아래의 경우 열의 개수는 3)로 나눠서, 그리드의 width 값을 구한다.
- 첫번째 그리드를 화면에 배치한다.
- 첫번째 그리드의 시작점(left, top)을 기준으로 잡고, 시작점의 left 값에 그리드의 width 값을 더해서 두번째 그리드의 시작점을 구한다.
- 현재 열의 top 값을 몇 번째 열의 그리드인지 구분해서 따로 저장해둔다.
- 첫번째 행을 꽉 채울 때까지, 3 ~ 4번을 반복한다.
- 두번째 행부터는, 저장해놓은 top 값을 이용해서 열의 높이가 가장 짧은 열에 새로운 그리드를 배치한다.
- 6번, 4번을 반복한다.
단순하게 리스트를 출력하는 데는 성능상 큰 이슈가 없어서 알고리즘에 대한 고민을 깊게 할 필요가 없었다. 그런데 모바일 환경의 경우에는 데스크톱에는 없는 변수들이 존재하기 때문에, 추가적으로 고민해야 할 부분이 몇 가지 있다. 그 중에 하나가 바로 기울기 전환시 대응이다.
기울기 전환시 성능 문제
JavaScript로 위치 값을 계산해서, 각 그리드의 위치를 잡아줬기 때문에, 기울기 전환이 일어났을 때 반응형으로 대응하려면 이 위치를 다시 계산해서 화면에 그려줘야한다. 그리드의 개수가 적을 때는 별 문제가 없는데 그리드의 개수가 늘어나면 성능이 급격하게 떨어진다.
이 문제를 어떻게 해결할 것인가를 두고 팀 원들이 모여서 논의를 했는데, 팀 동료인 최승학님이 다음과 같은 의견을 주셨다.
“처음 화면에 그리드를 그릴 때의 속도는 문제가 될 만큼 느리지 않다. 기울기를 변경했을 때, 새로 위치를 계산해야하는 그리드의 개수가 많은 게 문제다. 그렇다면 기울기 변경이 일어나기 전, 즉 처음에 그리드의 위치를 계산할 때, 미리 다른 기울기의 위치 값도 비동기적으로 계산해서 함께 가지고 있으면 어떨까?”
아주 좋은 생각이다 싶어 바로 적용하기로 했다.
Viewport 사이즈 예측
현재의 Viewport 사이즈를 구해서, 기울기 전환 시의 Viewport 사이즈를 예측해야 한다. 그래서 다음과 같이 현재 기울기에서 Viewport의 width * height 값을 구한 다음에 width와 height 값을 서로 바꿔주면 기울기 전환했을 때의 Viewport의 사이즈를 알 수 있을 거라고 생각했지만, 역시나 쉽게 될 리가 없다.
현재 기울기에서 Viewport의 width 값은 정상적으로 가져올 수 있지만, 기울기를 전환했을 때 width 값으로 사용할 현재의 height 값은 다음과 같은 이유로 정확하게 구하기가 어렵다.
- 단말기/OS/브라우저 별로 Viewport의 height 값을 가져올 수 있는 방법이 다르다. 심지어 아예 정확한 값을 구할 수 없는 단말기도 존재한다.
- 현재 Viewport의 height 값을 구한다해도 이 값이 상단 주소창, 하단 네비게이션 메뉴 등의 영향을 받는다.
이 모든 문제를 한 방에 풀어줄 수 있는 내장 객체가 있으면 좋겠지만, 찾지 못 했다. 그래서 값을 구할 수 없는 경우에는 단말기의 화면 비율(이 값을 구하는 과정도 파편화가…)을 이용해서 Viewport의 height 값을 예측하는 코드를 작성했다.
iOS의 경우 :
iOS의 경우에는 window.screen 객체의 width, height 값을 이용해서 Viewport의 정확한 너비를 구할 수 있다.
다만, 세로 모드일 때도 가로 모드 기준으로 값을 돌려 준다는 게 문제다. 그래서 현재 기울기가 어떤 상태인지 확인한 다음에 세로 모드일 경우에는 width와 height 값을 서로 교환해줘야 한다.
아래의 코드에서는 nOrientation이 0이면 portrait, 1이면 landscape라고 가정하고 있다. 이 값은 내가 임의로 정한 것이므로 편한 대로 변경해도 상관없다.
/* nOrientation이 0이면 portrait, 1이면 landscape. */ getViewportSizeOfIOS : function(nOrientation){ var oScreen = window.screen; if(nOrientation === 1) { return { nWidth : oScreen.height, nHeight : oScreen.width }; } return { nWidth : oScreen.width, nHeight : oScreen.height }; },
Android의 경우 :
안드로이드의 경우에는… 무슨 말이 필요하겠는가 ㅠ.
설명해보려고 했는데 케이스가 다양해서 어떻게 설명해야할 지 난감하다. 그래서 그냥 코드에 달린 간단한 주석으로 대신해야겠다.
getViewportSizeOfAndroid : function(){ var nDeviceWidth = window.screen.width; // 화면 너비 var nDeviceHeight = window.screen.height; // 화면 높이 var nViewportWidth = document.documentElement.clientWidth; // Viewport 너비 var nViewportHeight = document.documentElement.clientHeight; // Viewport 높이 /* window.outerWidth 값 보다 window.screen.width 값이 더 작을 때는 window.screen.width 값이 디바이스의 너비를 반영하지 못하는 있는 경우다. 이런 경우에는 window.outerWidth 값을 이용하는 게 정확하다. */ if(nDeviceWidth < window.outerWidth){ nDeviceWidth = window.outerWidth; nDeviceHeight = window.outerHeight; } /* 현재 화면의 높이인 nDeviceHeight 값과, 현재 Viewport 값을 의미하는 nViewportHeight 값이 같지 않은 경우에는 단말기의 물리적 width * height 비율을 이용해서 Viewport의 height 값을 예측하는 것이 더 정확하다. */ if(nDeviceHeight !== nViewportHeight){ nViewportHeight = Math.floor( (nDeviceHeight*nViewportWidth) / nDeviceWidth ); } return { nWidth : nViewportWidth, nHeight : nViewportHeight }; },
끝으로…
위의 방법으로 문제를 완벽하게 해결하지는 못한다. 그리드의 위치를 계산하는 시간은 줄였지만, 기울기 전환시에 재배치는 여전히 해줘야 하기 때문에, 그리드의 개수가 늘어날수록 속도가 느려진다. 여기에 무한 스크롤(Infinite Scroll)까지 적용하면 문제는 더 복잡해진다. 사실 이 부분이 더 어렵다. 이 UI를 모바일 웹에 구현하기 위해서는 해결해야 할 성능상 이슈가 너무 많다. 그래서인지, 이 UI를 적용한 모바일 웹 서비스도 흔치 않다. 두 개 정도 보긴 했는데, 만족스러운 수준은 아니었다. 어쨌든, 좀 더 좋은 성능을 얻기 위해서는 전체 그리드 개수를 적절하게 조절하는 로직을 추가해야한다. 이 부분은 아직 문제를 명쾌하게 풀어내지 못 했기 때문에, 혹시 다음에 기회가 되면 따로 포스팅을 해야겠다.
2개의 댓글
joontop · 2013년 11월 14일 11:21 오전
오 좋은정보 감사합니다.^^b
저도 오래전에 구현했던 로직이 하나있는데 성능이슈로 한 2년반정도 고민해온것 같네요..ㅋ
pinterest 가 알려지기도 전에 짠거라 소스가 좀 허접하긴 합니다..
설명) http://webpeace.net/content.jsp?num=94&posttype=92&userid=joontop&type=user
ex) http://www.webpeace.net/joontop/ex/shu02.html
얼릉 좋은방법이 나왔으며 좋겠네요~~
감사합니다.
김훈민 · 2013년 11월 16일 1:22 오전
우선… JS로는 한계가 있을 것 같고, CSS3가 빨리 자리를 잡기를 바래야 할 것 같아요. ㅎㅎ