Skip to content

[번역] UI는 좀 이따 생각해봅시다

Published:

이 글은 Michel Weststrate 의 UI as an afterthought을 원작자의 허락 아래 번역한 글입니다.

Michel Weststrate는 OSS 메인테이너이자 MobX, mobx-state-tree, Immer 등의 작성자입니다. 또한 독립적으로 교육자이자 컨설턴트로 활동하고 있습니다. 자세한 내용은 michel.codes를 참고하세요.

들어가기에 앞서: 본문에서 business process, process, business logic 등의 용어를 여러 번 혼용해서 사용하고 있습니다. 본 번역은 이에 충실하게 번역하되 읽으시는 분들은 혼란스럽게 받아들이시기 보단 뭉뚱그려 “비지니스 로직과 그 로직을 수행하는 과정” 으로 이해하시는게 좀 더 쉬우리라는 점 미리 말씀드립니다. 번역문에 대한 더 좋은 표현은 댓글로 남겨주시면 반영하겠습니다.


사람들이 제게 자주 묻는 질문 중에 하나가 “context(컨텍스트), hooks(훅), suspense(서스펜스)같은 리액트의 새 기능이 추가되었는데 앞으로 우리가 (웹) 앱을 만드는데 많은 영향을 미칠까요? 리액트 팀에서 Redux(리덕스)나 MobX(몹엑스)가 쓸모없어지는 새로운 상태 관리 라이브러리를 만들까요?” 같은 것입니다.

이 포스트에서 모든 질문에 한번에 대답해 드리겠습니다! 질문의 본질을 이해하기 위해 약간의 기초 지식을 다지는 시간이 필요합니다. 리액트, 리덕스, 몹엑스 이야기는 잠시 잊고 더 근본적인 질문에 답해 보도록 하죠.

웹 애플리케이션이 무엇일까요? 이 포스트를 작성하는 목적과 관련이 있는데, 웹 애플리케이션이란 그저(a) 사용자가 여러분의 비지니스를 사용할 수 있도록(interact) 제공되는 사용자 인터페이스입니다(user interface). 여기서 짚고 넘어가셔야 할 것은 그저 하나의(a) 사용자 인터페이스라는 점입니다. **유일한(the)**게 아닙니다. 잘 만들어진 프론트엔드의 목표는 걸리적거림이 적고 좋은 사용자 경험을 제공하며 이를 여러분의 비지니스 과정에 잘 녹여내는 것입니다. 하지만 프론트엔드가 비지니스 그 자체라고 보면 안됩니다!


사고 실험(thought experiment)으로서 구글 인박스가 더 이상 서비스되지 않는다고 가정해보죠(엇 잠시만요, 실제로 그렇게 될 예정이죠…😭 / 역주: 구글 인박스는 올 3월에 서비스가 종료될 예정입니다) 이론적으로 따지자면 사용자들은 전화기를 집어들고 구글에 전화해서 직원에게 “저기요, 제가 받아야 하는 메세지가 뭐가 있죠?” 라고 물어볼 수 있습니다. 이런 방식의 가정을 하는 연습은 여러분의 비지니스가 어떠한 영역에 닿아 있는지 떠올리는데 유용한 방법입니다. 사용자가 직접 사무실에 찾아온다면 여러분에게 무슨 질문을 할까요? 사무실에 불이 난다면 어떤 자료부터 지키려고 할까요? 궁극적으로 어떤 사용자 행위(interactions)가 돈을 벌어다 줄까요?

프론트엔드 개발을 하면서 종종 우리는 사용자 인터페이스를 다른 각도에서 접근합니다. 먼저 데이터가 없이 모형(mock-ups)을 만들고 거기다 동적인 상태를 붙여서 앱이 동작하도록 만듭니다. 이 방법을 사용하면 기본적으로 상태와 데이터는 나중에 신경쓸 일(afterthought)이 됩니다. 골치아프지만 미려한 UI를 만들기 위해 반드시 필요하지요. 보이는 부분부터 애플리케이션을 만들기 시작하면 필연적으로 “상태 관리(역주: State라는 단어를 썼지만 상태 그 자체 뿐 아니라 상태 관리에 이르는 개념이라는 맥락을 고려하여 이후에도 혼용합니다)가 만악의 근원이다” 라는 결론에 도달합니다. 상태 관리 때문에 처음에는 모든게 아름다웠다가 엉망진창 복잡해지게 되죠. 하지먼 그 발상을 뒤집어보겠습니다.

상태 관리가 모든 이익(revenue)의 근원이다.

하나 알려드리자면, 사용자가 여러분의 비지니스를 사용할 수 있게 만들어주는 모든 기회는 돈벌이와 직결됩니다. 네, 더 나은 UI 경험도 대부분 더 많은 돈을 벌어주죠. 하지만 직접 돈을 벌어주는 수단은 아닙니다.

그래서 겸허히 말씀드리건대 우리는 웹 애플리케이션을 다른 방향에서부터 만들어야 하고, 먼저 사용자가 시스템과 어떻게 상호작용할지 코드로 작성하는 일부터 시작해야 합니다. 앱을 사용하면서 어떤 과정을 거치게 되는지, 또 어떤 정보를 필요로 할지, 어떤 정보를 서버로 전송할지 등 달리 말하면 해결해야 하는 문제의 도메인(domain)을 모델링하는 작업부터 시작하는 겁니다.

이런 문제를 해결하는데 코드를 작성하는 일은 굳이 UI 라이브러리를 사용하지 않고서도 할 수 있습니다. 비지니스 로직을 수행하는데 필요한 동작을 추상적인 형태로 만들 수 있습니다. 그리고 단위 테스트를 하세요. 모든 비지니스 과정에서 어떻게 각기 다른 상태가 들어갈 수 있을지 깊이 이해하세요.

[트윗 번역]
애플리케이션의 (비지니스)로직을 다양한 UI에서 사용될 수 있도록 설계하고 개발하세요. 그럴 일이 없다 하더라도요. 메인스트림 프레임워크(리액트, 앵귤러, 뷰 등)가 의도치 않게 로직과 표현 계층이 결합되도록 유도하고 있는데, 이를 피할 수 있도록 강제합니다.

이 시점에서 아직 사용자가 어떤 환경의 도구를 사용하는지는 관계없이 비지니스와 상호작용을 할 수 있습니다. 웹 애플리케이션일까요? 리액트 네이티브 애플리케이션일까요? npm 모듈의 SDK일까요? 아니면 CLI(Command Line Interface)? 아무런 상관 없습니다! 따라서-

먼저, 웹 애플리케이션이 아니라 CLI를 만들듯이 상태와 스토어, 프로세스를 설계하세요.

그러면 이런 질문을 하실 수도 있습니다. “오버 엔지니어링이 되지 않을까요? 왜 내가 앱을 CLI를 만들듯이 해야하죠? 절대로 CLI를 만들 일이 없는데.. 저한테 약팔이(unicorn-puking)하시는거 아니에요?”

이제 잠시 이 포스트를 읽지 마시고 작업허시던 프로젝트를 열어서 테스트 러너를 돌려보세요. 그리고 다시 물어보겠습니다. 여러분의 앱에 CLI가 있나요? 없나요? 팀의 모든 개발자들이 CLI를 가지고 있을 겁니다.(그러길 희망합니다) 바로 테스트 러너 말입니다. 테스트 러너는 비지니스 프로세스를 실행하고 검증합니다. 단위 테스트가 프로세스와 상호작용 하는데 필요한 부가 설정(indirection, 역주: 아래에 나오지만 마운트, 랜더링, 이벤트 호출 등)이 적을수록 더 좋습니다. 단위 테스트는 시스템의 두 번째 UI입니다. 만약 TDD(Test Driven Development)를 하고 있다면 첫 번째 UI가 되겠죠.

리액트는 브라우저같은게 없어도 단위 테스트가 컴포넌트 UI를 이해하고 동작하기 아주 좋게 되어있습니다. 그래도 여전히 마운팅(mounting), 랜더링(rendering, 얕은 랜더링 혹은 전체 랜더링), 이벤트 호출(firing events), UI 스냅샷 뜨기(snapshotting UI) 등 별도의 부가 설정 없이 테스트를 할 수 있어야 합니다. 위의 부가 설정은 비지니스 도메인과 상관이 없으며 오히려 로직이 불필요하게 리액트와 묶이게 만듭니다.

비지니스 프로세스를 함수 형태로 직접 호출하는 것 만큼 단순한게 없습니다.

이 시점에서 왜 제가 언제나 도메인 상태를 직접 리액트 컴포넌트 상태로 집어넣지 말라고 주장했는지 실마리를 잡으셨을 겁니다. 비지니스 프로세스와 UI를 분리하고 불필요하게 앱이 복잡해지는 것을 막아주기 때문입니다.

만약 제 앱을 위한 CLI를 만들고 있었다면, 아마 yargscommander 같은 것을 썼을 겁니다. 그렇다고 제가 CLI로 UI를 만들겠다는 것도 아니고, 이 라이브러리들이 갑자기 제 비지니스 프로세스의 상태 관리를 맡아주는 것을 바라지도 않습니다. 달리 말하면 단순히 yargs 와 commander 를 바꾸는 정도의 코드 재작성은 충분히 감수할 수 있다는 겁니다. 리액트는 저에게 CLI나 같습니다. 사용자 입력을 받아서 프로세스를 실행하고, 그 데이터를 멋진 결과물로 보여주는 일을 합니다. 리액트는 사용자 인터페이스를 만들기 위한 라이브러리 입니다. 비지니스 프로세스를 다루는 라이브러리가 아닙니다.


사용자가 비지니스 프로세스를 실행한 것을 수집하고, 테스트하고, 확인한 뒤에야 실제 UI가 어떻게 표현되는지 알 수 있습니다. 어떤 기술을 사용하여 UI를 만들더라도 위에 이야기한 생각을 가지고 UI를 만들면 아주 편하다고 느끼게 될 것입니다. 컴포넌트를 만들면서 생각보다 상태(state)를 설정할 필요가 없다고 느끼게 됩니다. 어떤 컴포넌트는 자신의 상태를 가지고 있을 수도 있지만, 모든 UI 상태가 비지니스 프로세스와 관련된 것은 아닙니다. 예를 들어 현재 선택된 아이템, 탭, 라우팅 등 휘발성 있는 상태가 해당됩니다. 따라서-

대부분의 컴포넌트는 상태가 없는 컴포넌트(dumb)가 됩니다.

그리고 테스트가 훨씬 쉬워질 겁니다. 컴포넌트를 마운트하거나 이벤트를 일으키는 등의 테스트는 더 적게 작성하게 됩니다. 여전히 모든 컴포넌트와 상태가 제대로 연결되어있는지 확인하기 위해 어느 정도는 컴포넌트를 마운트하는 테스트가 필요하다고 느끼겠지만, 모든 발생 가능한 조합을 테스트할 필요는 없습니다.

UI와 비지니스 로직을 분리한 덕에 반복적인 UI 개선 작업이나 A/B 테스팅 등을 굉장히 빨리 할 수 있습니다. 한번 도메인 상태와 UI가 분리되면, 이후에 UI 구조를 개선하기 무척 쉬워지기 때문입니다. 에라이, 아주 완전히 다른 UI 라이브러리나 프로그래밍 패러다임으로 전환하는 것도 훨씬 싼 비용으로 처리할 수 있을겁니다. 상태 관리 부분은 대부분 영향을 받지 않기 때문이죠. 제가 봤던 대부분의 애플리케이션은 실제 비지니스 로직보다 UI에 들어다는 비용이 훨씬 더 높았기 때문에 분리 정책이 좋다고 봅니다.

제가 일하는 Mendix에서도 위의 정책을 적용하여 큰 성공을 거두었습니다. 데이터를 다루는 로직과 UI를 분리하는 패러다임은 모두가 자연스레 따르는 패러다임이 되었습니다. 예를 들어 보면, 사용자가 엑셀 시트를 업로드하는 기능을 구현한다고 할 때, 먼저 클라이언트 쪽에서 유효성 검사를 하고 서버와 통신한 뒤에야 프로세스를 실행합니다. 이런 새 기능은 먼저 순수한 JS 클래스를 만들어 매 프로세스의 단계별로 내부 상태와 메서드로 구현됩니다. 유효성 검사를 위한 비지니스 로직을 들고 있기도 합니다. 스토어의 진짜 상호작용은 백엔드와 일어납니다. 그리고 정확하게 유효성 검사 메세지가 출력되는지, 모든 프로세스가 상태 변화와 에러 조건에 따라 제대로 실행되었는지 확인하기 위해 단위 테스트를 작성합니다. 그 이후에야 개발자들은 UI를 만들기 시작합니다. 멋진 업로드 컴포넌트를 골라서 모든 과정에 필요한 폼(form)을 만듭니다. 그리고 유효성 검사 메세지의 표현이 좀 이상하면 고치기도 합니다.

이 시점에서 제가 왜 백엔드와 상호작용한 결과가 바로 UI에 녹아드는 방식을 그리 선호하지 않는지 아실겁니다. GraphQL을 다루기 위해 react-apollo 같은 바인딩을 사용하는 것 말이죠. 변경 사항을 반영하거나 데이터를 가져오는 등 백엔드와 상호작용하는 것은 제가 만든 도메인 스토어에서 담당해야 하는 일입니다. UI 레이어에서 처리할 일이 아닙니다. 제가 느끼기에 react-apollo는 데이터 계층과 UI 계층을 단단하게 결합하도록 만드는 지름길처럼 보입니다.


자 그럼 원래 질문으로 돌아가봅시다. “context(컨텍스트), hooks(훅), suspense(서스펜스)같은 리액트의 새 기능이 추가되었는데 앞으로 우리가 (웹) 앱을 만드는데 많은 영향을 미칠까요? 리액트 팀에서 Redux(리덕스)나 MobX(몹엑스)가 쓸모없어지는 새로운 상태 관리 라이브러리를 만들까요?” 라는 질문이었죠.

제 답변을 말씀드리겠습니다. 새 기능들이 상태 관리의 판도를 바꾸진 않습니다. context, hooks 기능이 리액트가 여태 못하던 것을 새로이 할 수 있게 만들어주는 트릭도 아닙니다. 그저 같은 트릭일 뿐이나, 훨씬 잘 정리되어있고 더욱 조합하기 쉬우며 에러가 덜 발생하도록 만들어졌습니다. (확실히 저는 새 API의 팬입니다!) 하지만 리액트 자체 API만 사용한다면 컴포넌트는 오로지 자체 상태(state)에만 반응할 수 있습니다. 만약 도메인 상태를 컴포넌트 트리 바깥에 두고 싶다면 별도의 상태 관리 패턴이나 추상화, 설계 구현체나 라이브러리를 사용해서 관리해야 합니다.

따라서 최근에 추가된 리액트 API는 근본적으로 상태 관리 상태계에 큰 변화를 일으키지 않습니다.

다시 말해 context, hooks 등이 나왔으니 리덕스나 몹엑스가 더이상 필요 없다고 느끼고 계신다면 이전에도 필요 없었을 겁니다.

hooks 에 대해 첨언하자면 이제는 컴포넌트의 지역 상태를 관리하기 위해 몹엑스를 쓸 필요가 줄었습니다. 특히 몹엑스의 observable 을 컴포넌트 상태로 사용한다면 suspense API를 활용할 수 없습니다.

일반적으로 suspense 대(versus) 상태 관리라는 주제로 이야기하자면 두 개념은 대립된다기 보단 관심사 분리를 명확하게 하기 위한 방법이라고 생각합니다. suspense + 리액트의 state 는 모든 UI 상태를 관리하기 아주 좋은 방법이고, 이를 활용하여 동시성 랜더링(concurrent rendering) 등을 할 수 있습니다. 동시성을 지원하는 것은 UI 상태같이 휘발성 상태에서 관리하는 것이 더 맞습니다. 하지만 비지니스 로직은 어떨까요? 비지니스 로직은 언제나 한 곳에서 관리되어야 합니다.

위에 말씀드린 내용을 바탕으로 모던 리액트 대 상태 관리 라이브러리에 대한 답을 드렸으리라 생각합니다.

리액트는 휘발성 UI 상태를 관리하고, 비지니스 로직을 관리해서는 안된다. 따라서 본질적으로 변한 것은 없습니다.

마지막으로 덧붙이자면 이 글을 통해 MobXmobx-state-tree에 근본 목표를 더욱 잘 이해하셨으리라 생각합니다. 이 라이브러리들은 아래와 같은 목표를 가지고 설계되었습니다.

  1. 어떤 UI 추상화 기법과 관계없이 상태를 관리할 수 있도록 한다.
  2. 깔끔하고 투명하게 상태를 UI와 연결할 수 있게 한다.
  3. 에러가 발생하기 쉬운 구독 관리(역주: pub-sub 패턴의 구독 이야기로 보입니다. 지난 포스트를 참고하세요), 셀렉터(역주: 메모이제이션 등. reselect 참고), 그 외의 직접 해야하는 최적화를 최대한 피하고 이벤트 발생 시 컴포넌트가 불필요하게 리랜더(re-render)되지 않도록 한다.

도메인 상태를 분리하여 관리하는게 얼마나 멋진 일인지 자세히 알고 싶다면 저의 “Complexity: Divide & Conquer” 발표를 보시거나 “How to decouple State and UI” 라는 글을 읽어보세요. Olufemi Adeojo 은 최근에 “The curious case of reusable state management” 라는 멋진 글을 써 주었습니다.

창을 닫기 전에 제 블로그를 한번 살펴보죠. 모든 블로거는 사용자를 끌어들이기 위해 (괜찮은) 이미지가 필요하다는 것을 알고 있습니다. 이 블로그는 기능적으로 그런 이미지를 고려하지 않았기 때문에 조금 못나 보이고 덜 최적화된 UI를 제공합니다. 하지만 블로그로써 제공할 수 있는 “비지니스 목표” 는 잘 동작합니다. 앞서 말씀드린 제 생각을 여러분들과 공유하는 것 말이죠. 특히 구현 관점에서 중요한 부분 말입니다.

UI는 그저 나중에 고려할 사항입니다(UI is just an afterthought).

UI as an afterthought

비지니스 프로세스를 통한 결과물이 스토어에 수집되었다면 그 자료를 이렇게 UI에 뿌려줄 수도 있습니다!

마지막 팁: 몹엑스를 쓰고 있고 리액트 버전 16.8.0 이상을 쓰고 계신가요? hooks API 기반의 mobx-react 바인딩을 살펴보세요. 기존 구현체보다 훨씬 적은 양의 코드가 쓰였습니다!

원본 글에 코멘트를 달고 싶으시다면 미디엄 페이지를 방문해주세요.