브라우저 밖에서 리액트를 다시 만나다
브라우저 탭 바깥에서 만난 리액트
저는 스타트업에서 정규직 개발자로 일하며 주로 리액트로 웹 서비스를 만들었고, 지금은 이런 저런 제조 대기업을 옮겨 다니며 상주 프리랜서로 일하고 있습니다. 직함과 계약 형태가 바뀌었을 뿐 쓰는 언어는 비슷할 것이라고 생각했는데, 생판 처음 겪어보는 제조업의 다양한 현장을 경험하다 보니 자바스크립트(타입스크립트)가 닿아 있는 영역이 제가 알던 것보다 훨씬 넓다는 것을 깨닫고 있는 중입니다. 설비 상태를 보여주는 모니터링 화면, 현장 작업자가 장갑을 낀 채 누르는 터치 패널, 장치가 보내는 신호를 모아 화면 쪽으로 넘겨주는 게이트웨이 서비스까지, 우리가 일반적으로 생각하는 웹 사이트라고 부르기는 어려운 것들이지만 많은 화면들이 웹 기반에서 웹 화면을 구성하는 언어와 도구 위에서 돌아가고 있었습니다. 이전 글에서 저는 “모든 웹 사이트에 정말 리액트가 필요한가”라고 물었는데, 어떻게 생각해보면, 웹 사이트를 웹이라는 환경 위의 html 문서들만으로 한정할 수는 없겠다는 생각이 듭니다.
프런트엔드라는 말이 가리키던 범위
프런트엔드, (많은 사람들이 ‘프론트엔드’라고 쓰지만 국립국어원이 공표하는 외래어 표기법에 의하면 정확히는 ‘프런트엔드’라고 쓰는 게 맞습니다…아무튼) 이 단어는 원래 사용자와 맞닿은 쪽, 클라이언트 영역, 말 그대로 “front-end”, 앞단의 끝, 을 의미할 뿐인데, 웹이 압도적인 플랫폼이 되면서 그 “앞단의 끝”은 곧 “브라우저 뷰포트”로 굳어버린 것 같습니다. 많은 사람들이 프런트엔드(FE) 개발자라고 하면 웹 화면을 만드는 개발자를 떠올리고, 실제로 많은 FE 개발자들이 웹 화면을 개발하고 있으니 그런 인식은 언뜻 자연스럽게 여겨지기도 합니다. 지금 제가 일하는 곳도 마찬가지여서, 이곳의 FE팀은 오로지 웹 화면을 담당하는 개발자들로 이루어져 있고, 저는 이곳에서 모바일과 그 외의 각종 장비 쪽 화면을 담당하지만 저는 FE팀이 아니라 FE팀과는 분리된 Native팀에 소속되어 있습니다. 같은 화면을 만드는 일인데도 이름과 소속은 갈려 있습니다. 사람이 소프트웨어와 만나는 면은 식당 테이블 위의 주문용 태블릿이기도 하고, 아파트 현관과 거실을 잇는 월패드이기도 하며, 주차장 출구의 무인정산기, 병원 접수 키오스크, 마트의 셀프 계산대, 공장 라인 옆 설비 제어 패널이기도 한데, 이 화면들의 공통점은 사용자가 “주소창에 무언가를 입력하지 않는다”는 것입니다. 전원을 켜면 정해진 화면이 뜨고, 사용자는 그 화면 안에서 몇 가지 조작을 하고, 기계는 그 조작에 반응하며 자기 상태를 다시 화면에 돌려줍니다. 사용자는 이것을 “웹사이트에 접속했다”고 느끼지 않고 “기계를 조작했다”고 느낍니다. 이런 화면을 만드는 개발자도 결코 적지 않을 텐데, 단순히 제가 느끼기엔 프런트엔드라는 이름은 이상하게도 유독 웹 브라우저 뷰포트 안의 화면 개발에 초점이 맞춰져 온 것 같다는 생각이 듭니다.
문서가 아니라 기계를 조작하는 화면
웹 문서와 기기 화면은 해결하려는 문제가 근본적으로 다릅니다. 웹 문서의 핵심 단위는 URL이 가리키는 문서이고, 그래서 검색엔진 노출, 링크 공유, 첫 바이트까지의 시간, 초기 HTML에 콘텐츠가 들어 있는가 같은 질문이 중요해집니다. 이전 글에서 길게 다룬 서버 렌더링, 스트리밍, 서버 컴포넌트, 캐시 계층은 모두 이 문서 모델 위에서 생긴 문제와 그 해법입니다. 반면, 예컨대, 주문용 태블릿을 예시로 들어보겠습니다, 여기에서 중요한 것은 /menu/coffee/americano라는 주소가 아니라 지금 사용자가 메뉴를 고르는 중인지, 옵션을 정하는 중인지, 결제 단말과 통신하는 중인지, 결제가 끝나 주문 번호를 보여주는 중인지입니다. 검색엔진 크롤러는 이 화면을 영영 방문하지 않고, 링크를 공유할 사람도 없으며, 검색 유입을 위해 초기 HTML을 어떻게 채울지 고민할 이유도 없습니다. 물론 전원을 켠 뒤 화면이 빨리 준비되어야 한다는 별도의 시작 속도 문제는 남습니다. 대신 터치하는 순간 화면이 바뀌어야 하고, 서버나 장치에서 오는 상태 변화가 지연 없이 화면에 반영되어야 하며, 몇 주씩 재시작 없이 켜져 있어도 화면이 망가지지 않아야 합니다. 즉 같은 리액트라도 웹 문서에서는 “서버가 이미 아는 내용을 어떻게 빨리 보여줄 것인가”가 숙제였다면, 기기에서는 “계속 변하는 상태를 어떻게 일관되게 화면으로 투영할 것인가”가 숙제가 됩니다. 그런데 이 두 번째 질문은, 재미있게도, 리액트가 처음 만들어질 때 세상에 던졌던 질문이기도 합니다.
이 부분에 대해 부연을 해보자면, 리액트가 어떤 문제에서 출발했는지를 이야기해야겠습니다. 리액트는 2013년 5월 JSConf US에서 처음 공개되었는데, 그 전에 페이스북 안에서 먼저 쓰이고 있었고 인스타그램이 그 뒤를 이었습니다. 찾아보니 리액트 초기 멤버였던 피트 헌트(Pete Hunt)는 ACM Queue와의 인터뷰에서 페이스북의 웹 앱 가운데 가장 복잡한 것으로 광고를 만들고 광고 계정을 관리하는 도구를 꼽았고, 그 화면을 Bolt라는 DOM 변화 감시 시스템으로 만들던 시절에는 아무도 원인을 풀지 못하는 버그가 쌓였다고 회고합니다. 읽는 문서라기보다 값을 입력하고 상태를 확인하며 계속 조작하는 도구에 가까운 화면이었을 것으로 짐작합니다. 이 문제에 대한 리액트의 답은, 헌트가 2013년 인터뷰에서 설명한 표현을 빌리면, 데이터가 바뀔 때마다 서버 렌더링 앱에서 새로고침 버튼을 누르는 것처럼 화면 전체를 다시 그린다고 생각하게 만드는 것이었습니다. 어디를 어떻게 고칠지 개발자가 일일이 지시하는 대신 지금 상태에서 화면이 어떤 모습이어야 하는지만 선언하고, 실제로 무엇이 달라졌는지는 가상 DOM이라는 내부 표현을 비교해서 리액트가 찾아냅니다. 정리하면 리액트의 첫 질문은 “이 문서를 얼마나 빨리 보여줄 것인가”가 아니라 “계속 바뀌는 상태와 화면이 어긋나지 않게 하려면 어떻게 해야 하는가”였고, 이 질문은 제가 생각하기에, 서버 렌더링과 검색 노출이 중요한 웹 문서보다 인터폰이나 주문용 태블릿 같은 기기 화면의 질문에 훨씬 가깝습니다. 그래서 기기 화면에서 리액트가 자연스럽게 맞아떨어지는 것은 우연이 아니라 설계의 출발점에 가까운 일이라고 저는 생각합니다.
이 차이는 화면 바깥의 네트워크 지식에서 더 선명해집니다. 일반적인 웹 프런트엔드에서 개발자가 직접 마주하는 네트워크 모델은 대체로 HTTP 기반의 요청과 응답을 중심으로 합니다. DNS 조회, TCP나 QUIC 연결, TLS 같은 하위 계층의 상당 부분은 브라우저와 인프라가 추상화해주고, 애플리케이션 개발자는 주로 상태 코드, 헤더, 캐시, 인증, CORS 같은 상위 계층을 다룹니다. 물론 웹소켓이나 SSE(서버가 브라우저로 이벤트를 계속 흘려보내는 HTTP 기반 방식)처럼 연결을 길게 유지하는 웹 서비스도 있고, 장애와 재연결이 설계 대상이라는 점은 웹도 마찬가지입니다. 다만 일반적인 웹 애플리케이션에서는 장비 통신에 비해 전송 계층 자체를 직접 의식할 일이 상대적으로 적습니다.
기기와 장비가 서로 통신하는 네트워크에서는 이 전제가 거의 모두 뒤집힙니다. 통신의 상대가 서버 한 대가 아니라 센서, 게이트웨이, 그 외의 기타 다른 기기이고, 프로토콜도 HTTP 하나가 아니라 시리얼 회선 위의 Modbus RTU나 이더넷 위의 Modbus TCP, 산업 표준인 OPC UA, 발행/구독 방식의 MQTT처럼 목적에 따라 갈립니다. 특히 Modbus 같은 프로토콜에서는 레지스터 주소와 바이트 순서를 직접 다뤄야 하고, 32비트 값을 두 레지스터에 나누어 담을 때 제조사마다 순서가 다르기도 해서 데이터시트를 읽고 값의 의미를 직접 해석해야 합니다. MQTT처럼 전달되는 내용의 형식을 애플리케이션이 정하는 프로토콜도 있고, OPC UA처럼 데이터에 의미 정보까지 붙여 모델링하는 훨씬 높은 수준의 표준도 있습니다. 통신 모델도 프로토콜마다 크게 달라서, Modbus처럼 요청과 응답이 명확한 프로토콜이 있는가 하면 MQTT처럼 브로커와 연결을 유지한 채 발행/구독으로 비동기 메시지를 주고받는 프로토콜도 있습니다. TCP 기반 프로토콜에서는 연결을 장시간 유지하는 경우가 있고, 시리얼 통신에는 애초에 TCP식 연결 개념이 적용되지 않습니다. 어느 경우든 케이블이 빠지거나 장비가 재부팅되어 통신이 끊기는 것은 예외가 아니라 일상입니다. 그래서 하트비트와 타임아웃, 재연결 정책, MQTT의 QoS 단계, Last Will 메시지처럼 “상대가 사라졌다는 사실을 어떻게 알아챌 것인가”를 다루는 지식이 중요해지고, 망 구성도 공인 인터넷이 아니라 폐쇄망 안에서 사설 인증서와 고정 IP, 시간 동기화까지 신경 써야 하는 경우가 흔합니다. 브라우저가 임의의 TCP 소켓이나 시리얼 포트를 자유롭게 열 수 없다는 보안 모델상의 제약 때문에, 웹 화면이 장비와 직접 대화하기보다 게이트웨이가 장비의 언어를 HTTP나 웹소켓으로 번역해주는 구조가 일반적이고, 이 번역의 품질이 화면에 보이는 상태의 신뢰도를 좌우합니다.
여기 나온 이름들이 낯선 분들을 위해 짧게 풀어두겠습니다. Modbus는 1979년에 만들어진 오래된 프로토콜인데, 한쪽이 “몇 번 장비의 몇 번 레지스터 값을 알려달라”고 물으면 상대가 숫자로 답하는 아주 단순한 질의응답 구조입니다. 레지스터는 번호가 붙은 작은 숫자 저장칸이라고 생각하시면 되고, 그 숫자가 온도인지 압력인지는 통신 규약이 아니라 장비 제조사가 제공하는 데이터시트의 표에 적혀 있습니다. 같은 질의응답을 RS-485 같은 시리얼 회선으로 주고받으면 Modbus RTU, 이더넷의 TCP 위에서 주고받으면 Modbus TCP입니다. OPC UA는 제조사가 서로 다른 장비와 소프트웨어가 데이터를 주고받을 수 있도록 만든 국제 산업 표준(IEC 62541)으로, 번호 붙은 칸이 아니라 이름과 자료형 같은 의미 정보가 붙은 객체 구조로 데이터를 표현하고 인증과 암호화도 규격에 포함하고 있습니다. MQTT는 발행/구독 방식의 메시징 프로토콜로, 센서가 브로커라는 중개 서버의 어떤 주제(topic)에 값을 올리면 그 주제를 구독하던 화면이나 서비스가 전달받으며, 양쪽이 서로의 주소를 알 필요가 없고 헤더가 가벼워서 불안정한 망에서도 널리 쓰입니다.
나머지 용어도 마찬가지입니다. 하트비트(heartbeat)는 “나 아직 살아 있다”는 신호를 일정 간격으로 주고받다가 신호가 끊기면 상대가 사라졌다고 판단하는 방식이고, MQTT의 QoS는 메시지가 최대 한 번(0), 최소 한 번(1), 정확히 한 번(2) 전달되도록 보장하는 정도를 고르는 단계입니다. Last Will 메시지는 장비가 연결을 맺을 때 브로커에 미리 맡겨두는 메시지로, 장비가 비정상적으로 끊기면 브로커가 대신 발행해서 “이 장비가 사라졌다”는 사실을 화면에 알려줍니다. VLAN은 하나의 물리적 네트워크를 여러 개의 논리적 2계층(데이터 링크 계층) 네트워크로 나누는 기술로, 서로 다른 VLAN 사이의 통신에는 라우팅과 별도의 정책이 필요하고, NTP는 장비들의 시계를 서로 맞추는 프로토콜입니다. 시계가 어긋나면 어느 장비의 기록이 먼저인지조차 알 수 없어서, 현장에서는 생각보다 중요한 설정입니다.
웹에서는 응답이 늦으면 로딩 표시와 재시도 버튼으로 대부분 해결되지만, 기기에서는 응답이 오지 않는다는 사실 자체가 하나의 상태이고, 그 상태를 정의하지 않으면 사용자는 멈춘 화면 앞에서 기계가 고장 난 것인지 통신이 끊긴 것인지 알 수 없습니다. 상태로부터 화면을 계산한다는 리액트의 모델이 기기 화면에서 더 절실한 이유가 여기에 있습니다.
상태 기계로서의 화면
리액트가 2013년 JSConf US에서 처음 공개되었을 때의 핵심 주장은, 화면을 직접 조작하지 말고 상태로부터 화면을 계산하라는 것이었습니다. 이 발상은 UI의 상태를 명시적으로 모델링하려는 더 오래된 여러 흐름과도 닿아 있습니다. 예를 들어 데이비드 하렐이 1987년 「Statecharts: A Visual Formalism for Complex Systems」에서 정리한 스테이트차트 모델이 그렇습니다. 웹 쪽에서는 2014년 페이스북이 공개한 Flux가 단방향 데이터 흐름을 대중화했고, 그 영향을 받아 등장한 Redux는 여기에 엘름(Elm) 아키텍처의 아이디어까지 받아들였습니다. 이 모델이 현장 화면에서 얼마나 자연스러운지는 인터폰 같은 장치를 떠올릴 때 가장 선명하게 느껴졌습니다. 대기, 호출 수신, 통화 중, 문 열림 처리 중, 네트워크 끊김처럼 화면이 가질 수 있는 상태가 유한하고, 상태마다 허용되는 조작과 보여줄 것이 다르기 때문입니다. 타입스크립트의 판별 유니언(discriminated union)을 쓰면 이 구조가 거의 그대로 코드가 됩니다.
type IntercomState =
| { kind: "idle" }
| { kind: "ringing"; visitorId: string }
| { kind: "talking"; visitorId: string; startedAt: number }
| { kind: "doorOpening"; visitorId: string }
| { kind: "offline"; retryAt: number };
function Screen({ state }: { state: IntercomState }) {
switch (state.kind) {
case "idle": return <StandbyView />;
case "ringing": return <IncomingCall visitorId={state.visitorId} />;
case "talking": return <VideoCall visitorId={state.visitorId} since={state.startedAt} />;
case "doorOpening": return <DoorProgress visitorId={state.visitorId} />;
case "offline": return <OfflineNotice retryAt={state.retryAt} />;
default: {
const _exhaustive: never = state;
return _exhaustive;
}
}
}
여기서 never 검사가 중요합니다. 요구사항이 바뀌어 상태가 하나 추가되면 그 상태를 처리하지 않은 모든 지점이 컴파일 단계에서 드러납니다. 현장에 설치된 기기를 쉽게 고칠 수 없다는 점을 생각하면 “빠뜨린 상태”를 배포 전에 잡아주는 이 성질은 편의가 아니라 안전장치에 가깝습니다. 여기에 useReducer로 상태 전이를 한곳에 모으거나 XState 같은 상태 기계 라이브러리로 허용 가능한 전이(transition)를 명시하면 존재해서는 안 되는 상태 전이를 훨씬 체계적으로 차단할 수 있고, 충분한 상태 전이와 이벤트 로그를 남겨두면 현장에서 어떤 순서로 이상 동작이 발생했는지를 추적하고 재현하는 데 큰 도움이 됩니다. 컴포넌트 구조도 마찬가지입니다. 카메라 영상 영역, 통화 상태 표시, 숫자 키패드, 문 열림 버튼, 볼륨 조절은 실제 기기에서도 독립적인 조작 단위여서 컴포넌트 트리가 제품의 구조를 거의 그대로 반영하게 됩니다. 블로그의 한 문단까지 컴포넌트로 만드는 것이 과하다고 느꼈던 저에게, 이 일치는 “추상화가 비용이 아니라 설계 도구가 되는 자리”를 보여주었습니다.
렌더러라는 이음매
리액트가 브라우저 바깥으로 나갈 수 있는 이유는 변경 사항을 계산하는 부분과 그 결과를 실제 환경에 반영하는 부분을 분리해 둔 구조에 있습니다. 리액트는 무엇이 바뀌었는지를 계산하는 reconciler와, 그 결과를 실제 환경에 반영하는 렌더러를 구분하며, 이 구분은 파이버 이전인 2015년 React Native가 처음 소개될 때도 이미 존재했습니다. 2017년 리액트 16에서는 기존 reconciler를 파이버(Fiber) 아키텍처로 전면 재작성했고, 이 구조를 이용해 별도의 렌더링 환경을 구현할 수 있게 해주는 react-reconciler 패키지도 존재합니다(다만 이 패키지는 공식 저장소에서 실험적인 커스텀 렌더러용 패키지로 안내되고 있습니다). 렌더러를 만드는 사람은 호스트 환경에서 노드를 생성하고 자식을 붙이고 갱신하는 방법 등을 정의하는 호스트 설정(host config)을 구현해야 하고, 상태 관리와 훅, 컴포넌트 합성 같은 나머지는 리액트가 그대로 제공합니다. 이 이음매 덕분에 react-dom은 브라우저 DOM을, React Native는 iOS와 안드로이드의 네이티브 뷰를, react-three-fiber는 three.js 장면을, Ink는 터미널의 텍스트 UI를 대상으로 삼습니다. 여기에 리액트 컴포넌트로 영상의 프레임을 구성하는 Remotion처럼, 컴포넌트 모델을 전혀 다른 출력물에 활용하는 도구도 있습니다. 레이아웃에는 React Native와 Ink가 모두 플렉스박스 레이아웃 엔진 요가(Yoga)를 활용하는 것으로 알고 있는데, 덕분에 웹에서 익힌 플렉스박스식 배치 감각을 모바일 앱이나 터미널 화면에서도 상당 부분 그대로 쓸 수 있습니다. React Native가 2015년 처음 소개될 때 내건 표어가 “write once, run anywhere”가 아니라 “learn once, write anywhere”였다는 점도 의미심장합니다. 같은 코드를 모든 곳에서 돌리겠다는 약속이 아니라, 같은 사고방식을 모든 곳에서 쓰겠다는 약속이었습니다. 이후 React Native는 JSI를 기반으로 Fabric과 TurboModules 등을 도입한 New Architecture를 0.76부터 기본으로 활성화했고(모바일 환경에 최적화된 자바스크립트 엔진 Hermes는 그보다 앞선 0.70부터 기본 엔진이었습니다), 마이크로소프트가 윈도우와 맥OS용 React Native를 따로 유지한다는 사실에서도 이 접근이 모바일에만 머물지 않음을 짐작할 수 있습니다. 결국 리액트에서 본체는 DOM이 아니라 “상태 변화를 최소한의 변경 목록으로 환산하는 계산”이었고, DOM은 그 계산 결과를 받아주는 여러 호스트 가운데 가장 유명한 하나였을 뿐입니다.
크롬이 들어 있는 기계들
현장에서 가장 자주 마주치는 구성은 사실 React Native가 아니라 “크로미움이 들어 있는 기계”입니다. 키오스크, 사이니지, 설비 옆 패널 PC는 상당수가 전체 화면 크롬이나 일렉트론, 혹은 임베디드 웹뷰 위에서 동작하는 웹 애플리케이션입니다. (지금 제가 상주 프리랜서로 근무하고 있는 곳에서도 저는 일렉트론을 다루고 있습니다.) 일렉트론(Electron)은 2013년 GitHub의 Atom 에디터를 위해 Atom Shell이라는 이름으로 시작되어 2015년에 개명되었고, 크로미움과 Node.js를 하나로 묶어 데스크톱 앱을 웹 기술로 만들 수 있게 해준 도구입니다. 슬랙과 디스코드, VS Code가 그 위에서 돌아간다는 사실은 이미 유명하지만, 같은 구조가 화면 하나짜리 전용 기기에도 그대로 들어간다는 점은 상대적으로 많은 이들이 모르는 사실입니다. TV 쪽도 비슷합니다. 찾아보니 LG전자는 webOS TV용으로 리액트 기반의 Enact라는 프레임워크를 오픈소스로 공개했고, 넷플릭스는 TV 기기용 UI에 리액트를 쓰기 위해 Gibbon이라는 별도 렌더링 계층을 썼다고 기술 블로그에서 밝힌 것으로 기억합니다. 웹 기술이 이렇게 기기로 번지는 데에는 이유가 분명합니다. 웹은 가장 널리 퍼진 UI 런타임이고, 개발자 풀이 넓으며, 디자이너와 개발자가 같은 언어(HTML과 CSS)로 소통할 수 있고, 화면 갱신을 네트워크로 배포하는 일이 네이티브 펌웨어를 다시 굽는 일보다 훨씬 가볍기 때문입니다. 2007년에 제프 앳우드(Jeff Atwood)가 남긴 “자바스크립트로 작성할 수 있는 모든 애플리케이션은 결국 자바스크립트로 작성될 것이다”라는 말은 농담 반 예언 반이었는데, 제조 현장에서 일하다 보면 이 문장이 꽤 진지하게 읽힙니다.
화면 뒤에서 일하는 타입스크립트
화면만 자바스크립트인 것은 아닙니다. 설비와 화면 사이에는 데이터를 모으고 가공하고 전달하는 층이 있는데, 이 층에서도 Node.js와 타입스크립트가 의외로 깊숙이 들어 있습니다. IBM에서 시작해 지금은 OpenJS 재단에서 관리되는 Node-RED는 자바스크립트로 만든 플로우 기반 도구로 IoT와 산업 현장의 데이터 흐름을 시각적으로 연결하는 데 널리 쓰이고, 경량 메시징 프로토콜인 MQTT(1999년 IBM에서 탄생해 2014년 OASIS 표준이 된)에는 브라우저와 노드 양쪽에서 쓰는 mqtt.js 같은 클라이언트가 있으며, 산업 통신 표준인 OPC UA(IEC 62541)에도 node-opcua 같은 구현이 존재합니다. 재미있게도 Node-RED 자체도 처음에는 MQTT 토픽 사이의 데이터 연결을 시각화하려는 작은 프로젝트에서 출발했다고 합니다. 2011년 RFC 6455로 표준화된 웹소켓은 이런 실시간 데이터를 브라우저나 웹뷰 화면까지 밀어 올리는 대표적인 통로입니다. 이 구조가 주는 이득은 언어 하나로 통일된다는 점보다 데이터 규약을 공유할 수 있다는 점에 있다고 생각합니다. 장치 메시지의 데이터 규약을 Zod 같은 런타임 스키마와 타입스크립트 타입으로 함께 정의해 공유 패키지로 관리하면 게이트웨이와 화면이 같은 정의를 참조하게 되고, 현장에서 필드 하나의 의미가 바뀌었을 때 그 변경이 퍼져나가는 지점을 컴파일러와 스키마 검증이 함께 알려줍니다. 타입스크립트의 타입은 실행 시점에 데이터를 검사해주지 않기 때문에, 장비에서 올라오는 값을 믿고 쓰려면 런타임 검증이 따로 필요합니다. 서로 다른 언어와 팀이 경계를 맞대고 있던 구조에서는 문서와 구두 합의가 하던 일을 스키마와 타입이 맡게 됩니다. 모니터링 화면의 대표 격인 그라파나(Grafana)가 프런트엔드를 리액트와 타입스크립트로 만든 대표적인 오픈소스 사례라는 점도, 이 조합이 산업 데이터를 다루는 화면과 잘 맞는다는 방증이 아닐까 생각합니다.
현장은 브라우저 탭보다 가혹하다
물론 웹 기술이 기기로 번졌다고 해서 웹 개발의 감각이 그대로 통하는 것은 아닙니다. 일반적인 소비자용 웹 페이지는 사용자가 잠깐 쓰다 닫는 경우가 많지만 기기 화면은 몇 주, 몇 달씩 종료되지 않은 채 동작하는 경우가 있어서, 작은 메모리 누수나 이벤트 리스너 중복 등록이 며칠 뒤에야 화면 멈춤으로 나타납니다. (……새벽 세 시의 메모리 그래프는 정직합니다.) 네트워크는 끊어지는 것이 정상 상태이므로 오프라인에서 어떤 화면을 보여주고 복구 후에 어떻게 재동기화할지가 설계에 들어가야 하고, 마우스 호버도 키보드 단축키도 없는 터치 환경에서는 버튼 크기와 간격, 오조작 방지, 유휴 시 초기 화면 복귀 같은 인터랙션 규칙을 처음부터 다시 생각해야 합니다. 저사양 기기에 크로미움 하나를 통째로 올리는 것은 메모리와 부팅 시간 면에서 만만한 선택이 아니어서, 자원이 빠듯한 기기에서는 웹 기술이 아닌 네이티브 방식이 여전히 합리적일 수 있고, 플러터 같은 다른 선언형 UI 도구도 기기 쪽으로 영역을 넓히고 있습니다. 무엇보다 중요한 선은 제어 루프입니다. 설비를 실제로 움직이고 멈추는 안전 관련 실시간 제어는 설비 전용 제어기의 몫이고, 결정론적 응답이 필요한 그런 제어를 가비지 컬렉션이 있는 일반적인 자바스크립트 런타임에 맡길 일은 아닙니다. 화면은 상태를 보여주고 명령을 전달하는 층이지 안전을 책임지는 층이 아니라는 구분만큼은 늘 의식하려고 합니다. 그러니 제가 말하는 것은 “현대 화면 개발은 리액트가 최고”라거나 “이제는 리액트의 시대도 끝나야 한다” 따위의 것들이 아니라, 그저 리액트가 어울리는 문제의 범위가 제가 생각했던 것보다 훨씬 넓었다는 것 정도입니다.
읽히는 문서인가, 조작되는 장치인가
이전까지 제가 피로감을 느꼈던 것은 리액트 자체가 아니라, 단순한 웹 문서를 보여주는 일에까지 애플리케이션 아키텍처를 들이대면서 그것을 정답처럼 여기는 분위기였던 것 같습니다. 문제의 크기와 도구의 크기가 어긋나는 데에서 오는 피로감이었다고 말할 수 있지 않을까요. 병원이나 자영업자의 소개 페이지처럼 단순한 화면에 하이드레이션 전략까지는 필요하지 않다는 생각은 지금도 여전합니다. 그러나 하루 종일 상태가 변하고, 사람이 끊임없이 만지고, 하드웨어와 서버가 번갈아 상태를 밀어 넣는 화면이라면 같은 도구가 전혀 다른 무게를 갖습니다. 그래서 요즘 저는 기술을 고르기 전에 질문 하나를 먼저 던집니다. 이 화면은 읽히는 문서에 가까운가, 조작되는 장치에 가까운가. 쇼핑몰처럼 둘이 섞인 경우가 많아 정답이 하나로 떨어지지는 않지만, 어느 쪽 성격이 강한지를 따져보는 것만으로도 SSR, 서버 컴포넌트, 상태 기계, 오프라인 전략 같은 선택지는 훨씬 빨리 정리될 수 있을 것입니다.
댓글
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.