Ray Book
에세이

React의 휴리스틱으로 바라본 공학적 사고

O(n³)을 O(n)으로 만든 React의 Diffing 알고리즘. 완벽한 정답 대신 충분히 좋은 답을 선택하는 공학의 본질에 대한 이야기.

essayreactalgorithmengineeringopinion

정답은 있는데, 쓸 수가 없다

두 개의 트리를 비교해서 최소한의 변환을 찾는 문제. 컴퓨터 과학에서 이미 풀린 문제다. 답은 존재한다. 다만 그 답의 시간 복잡도가 O(n³)이라는 게 문제다.

노드가 1,000개면 10억 번의 연산이다. UI를 매 프레임 갱신해야 하는 브라우저에서 이건 쓸 수 없는 답이다. 수학적으로는 정답이지만, 현실에서는 불가능한 답.

여기서 질문이 생긴다. 쓸 수 없는 정답은 정답인가? 이론의 세계에서는 "맞다"이지만, 공학의 세계에서는 "아니다"이다. 공학은 현실에 발을 딛고 있는 학문이니까.

불완전함을 선택하는 용기

React 팀은 이 문제 앞에서 이론적 완전성을 포기했다. 포기라는 단어가 부정적으로 들릴 수 있지만, 사실 이건 포기가 아니라 결단이다. 현실의 패턴을 관찰하고, 두 가지 휴리스틱 가정을 세웠다.

첫째, 타입이 다르면 다른 트리다. <div><section>으로 바뀌면, 자식을 하나하나 비교하지 않고 통째로 교체한다.

둘째, 같은 레벨의 자식은 key로 식별한다. 리스트에서 항목 순서가 바뀌었을 때, 내용을 대조하는 대신 key를 보고 "이 녀석이 저기로 갔구나"를 바로 파악한다.

이 두 가정만으로 재조정 알고리즘의 복잡도는 O(n³)에서 O(n)이 된다. 정확도를 약간 포기한 대가로, 수십억 배의 성능을 얻은 거다.

"완벽은 좋음의 적이다"라는 오래된 말이 있다. 완벽한 답을 고집하면 아무것도 만들 수 없다. 불완전하지만 충분히 좋은 답을 선택할 수 있느냐가 공학의 첫 번째 질문이다.

틀릴 수 있다는 걸 알면서도

여기서 진짜 흥미로운 건, React 팀이 이 가정이 틀릴 수 있다는 걸 알고 있었다는 거다.

타입이 달라졌는데 내부 구조가 거의 같은 경우? 분명 있다. 그런 경우 React는 불필요하게 트리를 통째로 다시 만든다. 이론적으로는 오답이다. 하지만 React 팀이 던진 질문은 "이게 맞느냐"가 아니었다. "실제로 이런 일이 얼마나 자주 생기느냐"였다.

이건 과학과 공학의 근본적인 시선 차이다. 과학은 진리를 묻는다. "모든 경우에 참인 것은 무엇인가?" 공학은 확률을 묻는다. "충분히 많은 경우에 작동하는 것은 무엇인가?"

과학자는 예외 하나에 이론을 수정한다. 공학자는 예외의 빈도를 보고 감수할지 말지를 판단한다. 둘 다 세상을 이해하는 방법이지만, 방향이 다르다. 과학은 세상을 있는 그대로 기술하려 하고, 공학은 세상을 있어야 할 모습으로 만들려 한다.

React의 두 가정은 "세상은 대체로 이렇게 생겼다"는 믿음 위에 서 있다. 그리고 그 믿음이 틀릴 수 있다는 걸 인정한다. 모든 지도는 실제 지형이 아니다. 하지만 쓸모 있는 지도는 있다. React의 휴리스틱은 완벽한 지도가 아니라, 쓸모 있는 지도를 선택한 결과다.

key, 기계가 인간에게 손을 내미는 지점

key prop은 이 철학이 개발자에게까지 확장된 지점이다.

{items.map(item => (
  <ListItem key={item.id} data={item} />
))}

React는 리스트 항목의 동일성을 완벽하게 추론할 수 없다는 한계를 스스로 인정한다. 그래서 그 판단을 개발자에게 넘긴다. "너만 key를 알려주면, 나머지는 내가 처리할게."

이건 작은 API 하나로 보이지만, 사실 깊은 인식론적 질문에 대한 답이다. 기계는 어디까지 알 수 있는가? 기계가 알 수 없는 영역은 어떻게 처리해야 하는가?

React의 답은 겸손하다. "나는 모른다. 그러니 네가 알려달라." 완벽한 자동화를 약속하는 대신, 자기 한계를 인정하고 인간과 협력하는 쪽을 택한 거다. 모든 것을 자동화하겠다는 오만함보다, 적절한 지점에서 책임을 나누는 겸손함이 더 나은 시스템을 만든다. key라는 작은 prop이 그걸 보여준다.

이 패턴은 어디에나 있다

이런 사고방식은 React만의 것이 아니다. 오히려 잘 만들어진 시스템이라면 거의 예외 없이 이 패턴을 따른다.

TCP의 혼잡 제어는 네트워크 상태를 완벽히 파악하지 않는다. 패킷 손실이라는 단서 하나로 전송 속도를 조절한다. 완벽한 정보 없이도 충분히 좋은 결정을 내리는 거다.

가비지 컬렉터는 "최근에 만들어진 객체가 먼저 죽는다"는 세대별 가설 위에서 동작한다. 모든 객체를 매번 검사하는 대신, 이 가설에 기대어 젊은 세대만 자주 훑는다. 가설이 틀리는 경우도 있지만, 맞는 경우가 압도적이니까 성립한다.

데이터베이스의 쿼리 옵티마이저도 마찬가지다. 가능한 실행 계획을 전부 시도하는 건 불가능하니까, 통계 기반의 추정으로 충분히 좋은 경로를 고른다.

패턴은 동일하다. 현실을 관찰하고, 규칙성을 발견하고, 그 규칙이 깨질 확률을 감수하면서도 실용적인 해법을 택한다. 그리고 규칙이 깨지는 드문 경우를 위해 탈출구를 마련해둔다. 이 탈출구의 존재가 중요하다. 자기 방식이 완벽하지 않다는 걸 인정해야 탈출구를 설계할 수 있으니까.

충분히 좋은 답의 가치

통계학자 George Box는 "모든 모델은 틀렸다, 하지만 그중 일부는 쓸모 있다"고 했다. 이 한 문장이 공학적 사고의 본질을 압축한다.

React의 휴리스틱도, TCP의 혼잡 제어도, 가비지 컬렉터의 세대별 가설도, 전부 "틀린 모델"이다. 하지만 현실에서 쓸모 있는 모델이다. 이 구분을 할 수 있느냐가 엔지니어의 역량이다.

모든 경우를 커버하는 알고리즘을 만드느라 아무것도 배포하지 못하는 것보다, 99%의 경우에 잘 작동하는 알고리즘으로 사용자에게 가치를 전달하는 것. React가 매 초 수백만 개의 컴포넌트를 거뜬히 갱신할 수 있는 건, 완벽한 답이 아니라 충분히 좋은 답을 선택했기 때문이다.

그리고 이건 코드를 넘어선 이야기이기도 하다. 설계를 하든, 제품을 만들든, 어떤 결정을 내리든, 우리는 항상 불완전한 정보 위에서 판단을 내려야 한다. 완벽한 정보가 모일 때까지 기다리면 영원히 아무것도 하지 못한다. 불확실함 속에서도 "지금 이 조건에서 가장 합리적인 것"을 골라 앞으로 나아가는 것. 그것이 공학이 하는 일이고, 우리가 매일 하는 일이기도 하다.