리액트의 시작

리액트는 페이스북의 고민을 해결했다.

리액트의 시작

1. 머릿말

 리액트가 어쩌다 탄생했는지, 리액트가 어떤 가치를 지향하는지 알아봅니다.

 MVC 디자인 패턴에서 리액트는 view를 담당하는데, 브라우저에서 우리 눈에 보이는 부분들은 HTML입니다. 하지만 리액트가 만드는 건 DOM이 아니라 객체입니다. 이는 리액트가 객체를 불변성으로 다루는 것과 연관이 있습니다.

자바스크립트와 메모리

 컴퓨터 성능은 복잡하고 큰 연산을 처리하는 데 중요한 요소입니다. 그러나 모든 연산을 대규모로 처리한다면 일반 작업이 비효율적이죠. 그래서 자바스크립트는 원시 타입(null, undefined, Number, String, Symbol, Boolean, Bigint)을 스택(Stack)에, 객체(Object, Array, Function...)는 힙(Heap)에 저장하고 사용합니다.
 스택은 작고 빠른 연산, 특히 함수 호출과 지역 변수 관리에 최적화되어 있습니다. 저장 용량이 작으며 스택에 직접 값을 저장하는 데이터는 메모리 할당량이 고정돼있습니다. 예를 들면 자바스크립트의 숫자는 변수당 64비트만 할당하며 IEEE 754 표준을 따릅니다. IEEE 754 표준에 따르면 숫자 중 1비트는 ± 부호를, 11비트는 바이어스 지수(Exponent bias)를, 52비트는 가수(mantissa)를 표현합니다.

이미지 출처: https://stackoverflow.com/questions/67767759/doubts-about-js-number-representation-and-his-bit-operation

 컴퓨터는 십진법 숫자가 주어져도 이진법으로 해석합니다. 이 때문에 0.1 + 0.2 !== 0.3이 되죠. 각 숫자를 이진법으로 정확히 계산할 수 없기 때문입니다. 또한 메모리가 64비트로 고정됐기 때문에 너무 큰 숫자를 표현할 수 없습니다. 그럴 땐 메모리를 동적으로 할당하는 bigint 타입으로 큰 숫자를 표현하곤 합니다. (bigint는 원시타입이면서도 동적 메모리 할당인 특이 케이스입니다.)
 한편, 힙은 크기가 동적인 데이터를 위해 설계되었습니다. 객체는 용량이 언제 어떻게 변할지 모르기 때문에 메모리 할당량을 고정하지 않습니다. 따라서 스택에 직접 저장하기엔 부적절합니다. 객체는 값을 힙에 저장하고, 스택에는 메모리 주소만 기록해둡니다. 이러한 구조 때문에 객체는 항상 "참조"됩니다.

이미지 출처: https://www.geeksforgeeks.org/memory-management-in-javascript/

 이러한 설계로 자바스크립트 엔진은 통상적인 실행 효율을 높이고, 복잡한 연산을 잘 처리합니다.
 모든 코드 처리와 데이터 접근은 스택에서 이루어지며, 스택에 직접 저장된 원시값은 즉시 사용할 수 있습니다. 스택에 값 자체가 저장됐기 때문입니다.

깊은 복사와 얕은 복사

 자바스크립트는 얕은 복사(shallow copy)와 깊은 복사(deep copy)라는 개념이 있습니다.

const detailInfo = { n: "991203-1231412", password: "1234" }
const user = { name: "John Doe", detailInfo }
const updatedUser = { ...user} // user의 detailInfo는 메모리 주소가 복사됨.
updatedUser.detailInfo.password = "4321";
console.log(updatedUser === user); // false
console.log(user.detailInfo.password); // 4321

 위 코드에서 user와 updatedUser는 서로 다른 객체입니다. 그러나 user와 updatedUser는 내부에 객체를 가집니다. 객체는 값 자체를 가지지 않고 메모리 주소를 가집니다. updatedUser의 detailInfo는 값이 아닌 메모리 주소를 복사해갑니다. 즉, user와 updatedUser는 똑같은 detailInfo를 공유합니다. 위 코드를 실행해보면 updatedUser의 비밀번호를 바꿨지만 user의 비밀번호도 바뀐 것을 확인할 수 있습니다.
 따라서 객체 내부에 객체 타입이 있고 정확한 복사가 필요할 경우 모든 객체를 조회해서 값을 확인하고, 값을 복사하는 수밖에 없습니다.

자바스크립트와 불변성

 불변성은 본래 원시값이 가지는 성질입니다.

이미지 출처: https://youtu.be/hM8s3ZaycGk?si=_eBCyuhCp8Ro7d1g

 원시값에 값을 재할당할 경우, 변수에 "바인딩된 값" 자체를 바꾸지 않고, 새로운 값을 생성해서 새로운 바인딩을 형성합니다. 비교 연산 과정을 생략하고 할당만 하는 게 더 효율적일 뿐더러, 데이터 변화를 명확하게 추적하기도 수월하기 때문입니다.
 만약 바인딩된 값 자체를 바꿔버리면 예전 값이 무엇이었는지 알 길이 없어집니다. 메모할 때 새로운 메모를 추가하면 예전 메모를 볼 수 있지만, 이전 메모를 지우개로 지우고 메모하면 원래의 메모들을 알 수 없는 것과 같죠.
 이러한 특성을 불변성이라고 짧게 요약합니다. 값 자체를 바꾸지 않는 성질. 불변(不變, immutable)하는 성질입니다.
 메모 예시로 알 수 있듯, 불변성은 데이터 변경 기록을 추적하는 열쇠입니다. 하지만 자바스크립트 객체는 가변성이며, 값이 변경되어도 메모리 주소값은 유지됩니다. 아파트 주민이 바뀐다고 아파트 주소지가 바뀌진 않는 것처럼요.
 좋은 개발이란 객체에 불변성을 부여해 변화를 잘 관리하는 것입니다. 즉, 원시값처럼 하면 됩니다. 객체 값이 변하는 등 변화가 생길 때마다 새로운 객체를 만들어서 변수에 새로 바인딩하는 것이죠.
 리액트는 컴포넌트로 UI를 작성합니다. 컴포넌트 크기는 상당할 테니 이러한 데이터를 스택에 저장할 순 없습니다. 리액트 컴포넌트는 불변성을 갖춘 객체이며, 리액트의 화면 업데이트란 새로운 객체를 만들어 새로 렌더링하는 작업입니다.

2. 페이스북의 고민

 페이스북은 옛날엔 XHP(2010년 페이스북이 만든 PHP, Hack의 확장판)를 사용했습니다. 모든 문맥을 이스케이프함으로서 XSS 공격을 예방하는 등의 목적이 있었습니다. (이 방식은 현재도 JSX에서 사용중.)

 하지만 한계가 있었습니다. 무슨 일이었을까요? 문제의 핵심은 동적 웹 어플리케이션이었습니다. XHP 만으론 훌륭한 동적 웹앱을 구현하기 어려웠습니다. 페이스북 유행 이전에는 LAMP 스택이라고 해서 Linux(운영 체제), Apache(웹 서버), MySQL(데이터베이스 서버), PHP(프로그래밍 언어)로 웹 어플리케이션을 서비스하는 시대였습니다. 클라이언트는 SSR(서버 사이드 렌더링)된 페이지를 가져와서 보여주되, 자바스크립트는 폼 제출 등의 간단한 처리만 하던 때였죠.
 페이스북은 사용자(클라이언트)와 상호작용이 많은 서비스입니다. jQuery로 실시간으로 DOM을 조작하고, Ajax로 클라이언트에서 서버로 요청을 보내는 등 자바스크립트 사용량이 많았습니다. 이용자수가 억 단위를 넘기면서 페이스북의 UI 변동은 더 잦아졌습니다. 일일이 DOM을 조작하는 건 너무 무거울 뿐더러 사용자가 이전 DOM에 저장했던 정보가 지워지는 등 여러 부작용(side effect)도 발생했습니다.
 XHP로는 이 문제를 해결할 수 없었고, 번들은 무거워지고, 페이스북은 점점 UX가 나빠졌습니다.

“무엇이 UI 구성을 어렵게 만들지?”

 당시 페이스북은 MVC 데이터 바인딩 시스템을 사용했는데 현대의 데이터 바인딩이 간단하지 않다는 점이 문제였습니다. 복잡성은 스노우볼 효과로 커지고 있었습니다. UI 구축은 테스트와 디버깅 등 고려할 요소가 많고, 똑같은 유지 보수에 개발자를 더 고용해야 할 만큼 개발자 경험마저 심각해 빠른 해결이 필요했습니다.

"simplicity is prerequisite for reliability"
"신뢰는 간결함을 전제한다."
-Edsger W. Dijkstra-

 대안 후보로 Key-Value Observation가 나왔습니다. iOS에서도 쓰는 데이터 바인딩 기법이지만 어플 전반적으로 관찰 시스템이 어떻게 동작하는지 이해를 요구했습니다.

 Angular의 더티 체킹도 고려했지만 KVO 보다 형편이 조금 나을 뿐, 모든 데이터가 최신 상태인지 정기적으로, 재귀적으로 확인해서 최선책은 아니었습니다.

 이들은 자바스크립트로 DOM을 내부적으로 관리하고, 필요할 때에만 진짜 DOM을 변경시키기로 했습니다. 가상 DOM의 시작입니다.

3. 가상 DOM

 “객체에 불변성을 줘서 기록을 추적하고 코드의 신뢰성, 안정성을 만듭니다.
이것이 가상 DOM의 핵심입니다.”

 “효율적인 리렌더링을 위해 엘리먼트를 생성, 비교, 업데이트하는 자바스크립트 DOM 모델.”

 “이 DOM 모델은 효율적인 생성과 읽기를 위한 디자인입니다. 우리가 실제 DOM 트리를 안 쓴 이유는 비용이 많이 들기 때문입니다. 일부 DOM 노드는 읽기만 해도 side effect가 일어나서 실제 DOM 노드로 바로바로 처리하는 것은 좋은 방식이 아닐뿐더러, 쉽지 않습니다.”

 “가상 DOM은 DOM 노드들을 표현하는 객체 집합입니다. 좀 기이하게 보이겠지만, "Document Object Model Model"란 말은 이 개념을 정확하게 표현합니다. DOM 트리를 표현하는 자바스크립트 트리. 우리는 이걸 VTree라고 부릅니다.”

 가상 DOM의 또 다른 장점은 누수가 발생해도 관리하기 쉽다는 점, 모바일에서의 퍼포먼스였습니다. 스마트폰의 대중화로 인해 모바일 최적화는 중요한 문제였고, 가상 DOM은 이를 훌륭하게 개선했습니다. KVO로는 퍼포먼스 개선이 불가능한 상태였지만 virtual DOM에선 메모이제이션과 캐싱 테크닉을 활용하기로 했습니다.

 “우리는 '컴포넌트를 기억해주길' 요청하는 훅을 제공합니다. 훅은 예전 데이터를 최근 데이터와 비교하고, 업데이트 여부를 반환하는 함수를 구현하죠”

 “가상 DOM의 핵심은 성능이 아닙니다. 간결함이죠.”

 “가상 DOM은 간결한 아키텍쳐를 지향하고 개발자 코드를 블랙박스처럼 취급하기 때문에 자바스크립트로 UI를 구축하는 표현법 중 단연코 제일이라고 생각합니다.”

 리액트는 가상 DOM을 이용해서 객체끼리 얕은 비교를 하고 갱신합니다. 처음에 적었듯이, 객체에 불변성을 부여해서 관리하기 때문에 가능한 작업입니다. 이 과정을 reconciliation(화해) 라고 부르며 diffing 알고리즘을 이용합니다.

 변화가 생길 때마다 객체 변화 흐름을 기록하니 문제가 생겨도 어디인지 찾기 쉬워졌고, UI를 쉽게 조작하고 코드 작성, 테스트와 디버깅도 개선됐습니다. 또한 리액트는 컴포넌트 기반으로, 컴포넌트별로 관심사를 분리하는 원칙을 지니고 있습니다. 이 또한 개발자 경험에 도움이 됐다고 할 수 있습니다.

 페이스북은 비로소 앓던 문제를 해결했습니다. 원래라면 리액트를 페이스북 개발에만 쓸 예정이었지만 2012년 meta(구 페이스북)이 인스타그램을 인수하면서 인스타그램 개발에도 리액트를 쓸 수 있도록 손을 더 봤고, 그 결과 2013년 JSConf에서 오픈소스 JS 라이브러리로 나오게 됩니다.

 주의할 점은 리액트는 빠른 속도와 성능 때문에 태어난 개념은 아닙니다. Svelte만 보더라도 리액트보다 훨씬 빠릅니다.
 리액트의 핵심 가치 중 하나는 쾌적한 UX 제공입니다. 예를 들면 useMemo가 오히려 최적화를 방해해서 UX를 저해하는 경우가 있습니다. 그래서 리액트 2021 컨퍼런스에선 메모이제이션 일부 값을 잊어버리는 React Forget이란 주제로 해당 내용을 다뤘습니다. 최근에 발표한 React 18 버전 역시 UX에 초점이 맞춰져있습니다.

4. 마무리

 리액트는 UI 변동이 잦은 서비스에서 쾌적한 UX 제공 및 안정적인 유지보수를 위해 만든 라이브러리일 뿐, 해당사항이 별로 없다면 리액트를 고집할 필요는 없습니다.
 상술한 문제들은 그 시절의 이슈입니다. 거진 10년 전 이야기입니다. 당시에는 리액트가 혁신이었지만 2024년 현재 여러 라이브러리, 프레임워크가 기존 문제를 많이 개선했고 새로운 도구들도 많습니다. 오히려 리액트로 개발하겠다고 여러 의존성 설치를 하다보면 리액트가 힘들게 느껴질 때도 있습니다.
 따라서 라이브러리나 프레임워크를 도입할 땐 현재 팀에 어떤 게 필요한지 고려해보길 권합니다.

5. 참조