UI 엔지니어링의 요소들


이 포스트는 The Elements of UI Engineering를 번역한 글입니다.


이전 포스트에서 저는 우리의 지식의 빈틈을 인정하는 것에 관해 이야기했습니다. 제가 평범함에 안주하라고 권유하는 것처럼 보이실 수도 있겠지만, 아닙니다! 이곳은 광범위한 분야입니다.

저는 여러분이 “어디에서나 시작할 수 있고”, 기술을 특정 순서로 배울 필요가 없다고 굳게 믿고 있습니다. 하지만 저는 전문지식을 얻는 것에도 큰 가치를 두고 있습니다. 개인적으로 요새 UI를 만드는 데에 푹 빠져있지요.

저는 요즘 제가 알고 있는 게 무엇이고 어떤 것에 가치를 두는지에 대해 많은 생각을 하고 있습니다. 물론, 자바스크립트나 React 같은 기술에 익숙하긴 하지만 경험으로부터 얻은 더 중요한 교훈들을 찾아보기 힘듭니다. 이러한 교훈들을 글로 적으려고 시도조차 하지 않았구요. 그래서 이 중 일부를 한번 적어보려고 합니다.


찾아보시면 기술 혹은 라이브러리를 어떻게 배울지에 관한 “로드맵”들이 많이 있다는 것을 아실 수 있을 겁니다. 2019년에는 어떤 라이브러리가 유행할까요? 2020년에는요? (참고로, 원문은 2018년 12월에 작성되었습니다!) Vue, React, Angular 중에 어떤 걸 배워야 할까요? Redux도 배워야 할까요? Rx는요? 이러한 것들 속에서 길을 잃기 십상입니다. 만약 글쓴이가 틀렸다면 어떨까요?

제 학습에서 가장 큰 혁신은 기술에 관한 것이 아니었습니다. 그보다, 특정 UI 문제를 해결하려 고민할 때 많은 것을 배울 수 있었습니다. 때로는 이후에 제게 도움이 되는 라이브러리나 패턴을 발견할 수 있었고, 그렇지 않은 경우엔 좋은 해답이든 나쁜 해답이든 저만의 해답을 생각해냈었습니다.

문제를 이해하고, 해답을 통해 실험하고, 서로 다른 전략들을 적용해보는 과정들이 제 인생에서 가장 보람된 학습 경험이었습니다. 이 포스트에서는 “문제”에 집중할 것입니다.


UI를 만들어 보신 경험이 있으시다면 앞으로 살펴볼 문제 중 일부를 경험해 보셨을 겁니다. 직접 하셨든 라이브러리를 사용하셨든 간에, 라이브러리를 사용하지 않고 작은 앱을 만드셔서 이러한 문제들을 재현해보고 해결해보시는 것을 추천합니다. 문제의 해답이 오직 하나만 존재하는 것은 아닙니다. 배움은 문제를 탐구하고 서로 다른 해답들의 장단점을 저울질해나가는 과정에서 온다고 할 수 있습니다.

일관성 (Consistency)

“좋아요” 버튼을 누르면 텍스트가 “회원님과 3명의 다른 친구가 이 포스트를 좋아합니다.”로 바뀝니다. 한 번 더 누르게 되면 텍스트는 다시 이전으로 돌아갑니다. 쉽죠?

하지만 이와 같은 라벨은 화면의 여러 군데에 존재할 수 있습니다. 버튼의 배경과 같이, 변경되어야 하는 시각적인 표시가 있을 수도 있습니다. 이전에 서버로부터 가져온 “좋아요를 누른 사람들”의 목록에 여러분의 이름을 포함해야 하고, 다른 화면으로 이동했다가 다시 돌아와도 여러분이 해당 포스트의 좋아요를 눌렀다는 점을 표시해야 합니다.

지역 일관성(local consistency) 하나만으로도 여러가지 문제가 발생할 수 있습니다. 하지만 제가 보고 있는 화면의 데이터를 다른 유저가 변경할 수도 있습니다 (상대방이 제가 보고 있는 글의 좋아요를 누르는 것과 같이요!).

어떻게 하면 화면의 서로 다른 부분에 존재하는 동일한 데이터의 싱크를 맞출 수 있을까요? 언제, 그리고 어떻게 로컬 데이터를 서버와 일관되게 만들 수 있을까요? 다른 방법도 있을까요?

반응성 (Responsiveness)

사용자들은 자신의 행동에 대한 시각적인 피드백이 나타나지 않는 것을 잠깐동안만 참아줄 수 있습니다. 제스처 혹은 스크롤과 같은 연속된 동작에 대해선 인내할 수 있는 시간이 더욱 짧겠죠. 16ms짜리 한 프레임만 소실되어도 “버벅”인다고 느끼는 정도니까요.

사용자들이 클릭과 같은 불연속적인(discrete) 동작에 대해선 100ms 이하의 딜레이는 빠르다고 느낀다는 연구 결과가 있습니다. 동작을 처리하는 데 시간이 오래 걸리게 된다면 로딩 스피너와 같이 시각적으로 무언가를 보여줘야 합니다.

하지만 우리의 직관에 반하는 문제들도 존재합니다. 페이지를 급작스럽게 건너뛰거나, 혹은 여러 로딩 “단계”를 거치는 듯한 느낌을 주게 되면 동작이 실제보다 더 느려 보이게 될 수 있습니다. 이와 유사하게, 프레임을 저하시키면서 20ms 안에 동작을 처리하는 것은 프레임을 저하시키지 않으면서 30ms 안에 동작을 처리하는 것보다 느리다고 느끼게 될 수 있습니다.

우리의 뇌는 벤치마크가 아닙니다! 어떻게 하면 우리의 앱이 서로 다른 입력에 대해 지속적으로 빠르게 대응하도록 할 수 있을까요?

지연 (Latency)

계산과 네트워크 접근 모두 시간이 소요되는 작업입니다. 우리가 타겟으로 설정한 장치에서 이러한 작업을 수행했을 때 반응성이 저하되지 않는 경우라면 이러한 비용은 무시할 수 있습니다 (반드시 저사양 기기에서도 앱을 실행해서 테스트해보셔야 합니다!).

하지만 네트워크 지연은 필연적입니다. 심지어 수 초가 걸릴 수도 있죠. 그렇다고 우리의 앱이 네트워크를 통해 데이터나 코드를 받아오는 동안 무작정 기다리고 있을 수만은 없습니다. 즉, 새로운 데이터, 코드 등을 필요로 하는 작업들은 비동기적으로 일어날 수 있기 때문에 “로딩 중”인 상태에 대한 처리가 필요합니다.

이러한 일이 거의 모든 화면에서 발생할 수도 있기에, 어떻게 하면 연속적인 스피너 혹은 구멍 (스피너 뒤에 또 스피너가 나오고, 또 다음 스피너가 나오고, ..) 을 표시하지 않으면서 이와 같은 상황을 처리할 수 있을까요? 어떻게 하면 “급변하는(jumpy)” 레이아웃을 피할 수 있을까요? 그리고 어떻게 매번 우리의 코드를 다시 짜지 않으면서 비동기 의존성을 변경할 수 있을까요?

탐색 (Navigation)

우리는 사용자가 UI와 상호작용 하더라도 계속해서 “안정적인” 상태를 유지할 것이라 예상합니다. 무언가 코앞에서 사라져버려선 안됩니다. 탐색을 앱 내부에서 시작했든 (e.g. 링크 클릭), 혹은 외부 이벤트에 의해 실행했든 간에 (e.g. 뒤로가기 버튼 클릭) 이 원칙을 지켜야 합니다. 예를 들면, 프로필 페이지 내의 /profile/likes 탭에서 /profile/follows 탭으로 이동했다고 하더라도 탭 바깥에 있는 검색창의 입력값을 지우면 안 됩니다.

다른 화면으로 탐색하는 것조차 방에 걸어 들어가는 것과 비슷합니다. 사람들은 다시 되돌아와서 두고 온 것들 (혹은 새로운 것들)을 찾을 수 있을 것이라 생각합니다. 만약 여러분이 현재 피드의 중간에 위치한 상황에서 프로필을 클릭하고 다시 되돌아왔을 때 기존의 위치가 초기화된다든지 아니면 피드를 다시 불러와야 되면 아마 짜증 날 것입니다.

이렇듯, 어떻게 하면 중요한 컨텍스트를 유지하면서 임의의 탐색을 잘 처리할 수 있을까요?

캐시 (Staleness)

우리는 로컬 캐시를 이용하여 “뒤로 가기” 버튼을 만들 수 있습니다. 빠른 접근을 위해, 캐시를 통해 몇몇 데이터를 “기억”할 수 있습니다. 이론적으론 다시 가져올 수도 있지만요.

하지만 캐싱에도 문제는 있습니다. 바로 캐시가 상할 수 있다는 것이죠 (stale). 제가 만약 아바타 이미지를 바꾸면 캐시도 같이 업데이트를 해야 합니다. 새로운 포스트를 썼을 때 이 글을 캐시에 즉각적으로 반영하거나, 캐시를 무효화(invalidate) 해야 합니다.

이는 어렵기도 하고 에러가 쉽게 발생할 수 있는 문제입니다. 만약 포스팅에 실패했다면요? 캐시는 얼마나 오랫동안 메모리에 남아있어야 할까요? 피드를 다시 가져왔을 때 새로 가져온 피드를 캐시 된 피드에 추가해야 할까요, 아니면 캐시를 버려야 할까요? 페이지 네이션이나 정렬은 어떻게 캐시로 나타낼 수 있을까요?

엔트로피 (Entropy)

정확하진 않지만, 열역학 제 2법칙에 의하면 “무질서는 항상 증가한다”고 합니다. 이는 UI도 마찬가지입니다. 우리는 사용자가 어떤 상호작용을 어떤 순서로 발생시킬지 정확하게 예측할 수 없습니다. 우리는 결과를 예측할 수 있게, 그리고 우리의 설계 내에 존재하도록 최선을 다해야 합니다. 버그 스크린샷을 보고 “이게 왜 이렇게 되지?” 라고 하고 싶으시진 않을 테니까요.

N개의 가능한 상태 간에는 N(N-1)개의 전환이 있습니다. 예를 들어, 버튼이 normal, active, hover, danger, disabled의 5가지 상태를 가진다고 할 때 버튼을 업데이트하는 5×4=20 경우의 수에 모두 대비가 되어있어야 합니다.

가능한 상태들의 무수한 조합을 어떻게 관리하고 시각적인 결과를 예측할 수 있게끔 만들 수 있을까요?

우선 순위 (Priority)

어떤 것들은 다른 것보다 더 중요합니다. 버튼을 눌렀을 때 모달창이 뜨는 경우, 이 모달창은 컨테이너의 경계를 뚫고 나와서 버튼보다 위에 위치해야 할 수 있습니다. 새로 예정된 작업(e.g. 클릭에 반응)은 오랫동안 실행 중인 작업(e.g. 접힌 화면의 밑에 다음 포스트를 렌더링)보다 더 중요할 수 있습니다.

우리의 앱이 커짐에 따라, 서로 다른 사람 혹은 팀에 의해 작성된 코드들은 CPU, 메모리, 네트워크, 스크린 상태, 제한된 번들 크기와 같이 한정된 리소스를 두고 경쟁하게 됩니다. CSS의 z-index와 같이, 때로는 경쟁자들에게 “중요도”에 따라 순위를 매길 수도 있습니다만, 잘 되는 경우는 드뭅니다. 모든 개발자들은 자기의 코드가 더 중요하다는 편견을 가지고 있으니깐요. 그리고 모든 것이 중요하다면, 오히려 모든 것이 중요하지 않을 수도 있습니다.

어떻게 하면 자원을 차지하려고 싸우기보다, 서로 협력하는 독립적인 위젯들을 만들 수 있을까요?

접근성 (Accessibility)

접근성이 떨어진다는 것은 사소한 문제가 아닙니다. 예를 들어, 영국에서는 5명중 1명 꼴로 장애의 영향을 받습니다 (인포그래픽). 제 개인적으로도 이걸 느꼈습니다. 저는 26살이지만, 폰트가 가늘고 대비(contrast)가 낮은 웹사이트들을 읽기 어렵습니다.

어려움이 있는 사람들도 우리의 앱을 쉽게 사용할 수 있도록 노력해야 합니다. 좋은 소식은, 이것이 어렵지 않다는 점입니다. 우선은 교육과 도구로 시작합시다. 하지만 제품 개발자들 또한 옳은 일을 쉽게할 수 있도록 해야합니다.

접근성을 부가적인 문제로 인식하는 것이 아니라, 기본적인 것으로 만들기 위해 어떻게 해야할까요?

국제화 (Internationalize)

우리의 앱은 세계 어디서든 잘 동작해야 합니다. 각국의 언어를 지원하는 것 뿐만 아니라, 오른쪽에서 왼쪽으로 읽는 레이아웃 또한 지원해야 합니다.

어떻게 하면 지연과 반응성을 희생하지 않으면서 다국어를 지원할 수 있을까요?

전송 (Delivery)

사용자의 컴퓨터에서 우리의 코드를 가져오도록 해야 하는데, 어떤 전송 방식과 형식을 사용할 수 있을까요? 당연하게 들리겠지만 여기엔 많은 트레이드 오프가 존재합니다.

예를 들어, 네이티브 앱들은 거대한 앱의 크기를 감수하고서라도 사전에 모든 코드를 미리 다운받아 놓는 경우가 많습니다. 반면 웹 앱들은 사용 중의 지연을 감수하고서라도 최초에 가져오는 데이터의 크기를 작게 하는 경우가 많죠.

그렇다면 과연 어떤 부분에서 지연을 발생시켜야 할까요? 어떻게 하면 사용 패턴에 기반해서 전송하는 방법을 최적화할 수 있을까요? 최적의 해결책을 위해 어떤 데이터들이 필요할까요?

회복력 (Resilience)

만약 여러분이 곤충학자라면 “버그”를 좋아하실 수도 있겠습니다만(😂), 아마 프로그램에서 버그를 보고 싶지는 않으실 겁니다. 실제 제품엔 필연적으로 몇몇 버그들이 들어가게 될 겁니다.

그럼 어떤 일이 일어날까요? 몇몇 버그들은 잘못되었지만, 잘 정의된 동작을 유발하게 됩니다. 예를 들면, 어떤 상황에 대해 잘못된 UI를 표시할 수도 있습니다. 하지만 렌더링 된 코드가 깨지는 경우는요(crash)? 그렇게 된다면 출력된 UI가 일관적이지 않으므로 의미 있는 동작을 진행할 수 없게 될 것입니다. 하나의 포스트를 렌더링하지 못했다고 해서 전체의 피드가 전부 망가지거나, 추가적인 고장을 유발하는 반쯤 고장난 상태가 되어선 안 됩니다.

어떻게 하면 렌더링으로 인해 일어나는 에러와 fetching으로 인해 일어나는 에러를 분리하고, 앱의 나머지 부분은 정상적으로 돌아가게끔 할 수 있을까요? UI에게 고장 허용 한계(fault tolerance)란 어떤 의미일까요?

추상화

아주 작은 규모의 앱에선 위와 같은 문제들을 처리하기 위한 많은 특수 케이스들을 하드 코딩할 수 있습니다. 하지만 앱은 커지기 마련이죠. 우리는 코드를 재사용하고(reuse), 나누고(fork), 합치길(join) 원하며, 또한 공동으로 작업하길 원합니다. 더욱이, 우리는 서로 다른 사람에게 친숙한 부분들 사이에 명확한 경계를 설정하고, 자주 변경되는 부분을 너무 엄격하게 만들길 원하지 않습니다.

어떻게 하면 특정 UI 부분의 세부적인 구현사항을 숨기는 추상화를 만들 수 있을까요? 우리의 앱이 커짐에 따라 이미 해결했던 문제를 다시 만들어내지 않으려면 어떻게 해야 할까요?


물론, 제가 언급하지 않은 문제들이 더 많이 있습니다. 위의 목록은 완전하지 않습니다. 예를 들자면, 디자이너와 개발자의 협업이라든가 디버깅, 테스팅과 같은 부분이 있을 수 있겠지요. 다음에 시간 나면 쓰겠습니다 😅

특정 view 라이브러리나 데이터 fetching 라이브러리를 위 문제들에 대한 해결책으로 염두해 둔 채 이 글을 읽으셨을 수도 있겠습니다. 하지만 개인적으론 그러한 라이브러리들이 존재하지 않는다고 생각하시고, 신선한 관점으로 다시 살펴보세요. 이제는 어떻게 위 문제들을 해결하실 건가요? 작은 앱을 만들어 실험해보세요! (만약 만드셨다면 트위터를 통해 저한테 알려주시구요 😁)

흥미로운 점은, 이와 같은 문제들이 앱의 규모에 상관없이 발생한다는 점입니다. 자동완성이나 툴팁같은 작은 위젯부터 트위터, 페이스북과 같은 거대한 앱에서 위 문제들을 보실 수 있습니다.

여러분이 즐겨 사용하시는 앱의 중요한 UI를 머릿속에 담아둔 채 위 목록을 다시 읽어보세요. 해당 앱의 개발자들이 어떤 트레이드 오프를 선택했는지 설명하실 수 있나요? 그와 비슷한 동작을 구현해보세요!

저는 어떠한 라이브러리도 쓰지 않고 작은 앱을 만들면서 위 문제들을 실험했을 때 UI 엔지니어링에 대해 많이 배울 수 있었습니다. UI 엔지니어링의 트레이드 오프에 대해 깊은 이해를 얻고자 하신다면 이와 같은 방법을 추천합니다.