Before React: 리액트 이전의 웹 개발

Yunhye Park·2026년 1월 31일

Back To Basic

목록 보기
12/12

들어가며

“이거 어떻게 해?”

한 마디만 물으면 순식간에 답을 내놓다 못해 알아서 만들어주는 대AI 세상은 모든 것이 쉬워만 보인다. 달리 말하면 개발자의 의의가 점점 더 협소하게 느껴지기 쉽다. 하지만 어느 때고 나아가는 움직임은 존재하는 법. 달리 말하면 방향성이겠는데, 나는 이럴수록 기본에 충실해야 한다고 본다.

실무 초반엔 이런 게 중요해 보인다: 리액트의 온갖 훅을 사용해서 어떤 기능을 만들고, 전역적으로 에러 캐치를 해서 토스트 모달을 띄우고, ... 등 화려하고 세밀해보이는 작업들. 그러나 가장 좋은 코드는 작성하지 않은 코드라는 표현을 보고, 그 말을 곱씹게 된 요즘이다. 소프트웨어는 완성이 곧 시작이다. 이전의 코드들을 토대로 앞으로의 변화를 감당해야 하니까 말이다.

언제나 마감기한은 빠듯하고 요구사항은 변화한다. 일을 쳐내기 바쁜 와중에는 일단 동작하게 만드는 것이 최우선인 건 별 수 없다고 본다. 다만 그 바쁜 와중에도 무엇이 중요한지를 안다면 최소한의 토대는 챙길 수 있다고 본다. 그 최소의 기반은 뭘까.

당연하게 생각하는 것부터 파고드는 게 시작점이 아닌가 싶다. 그래서 매일같이 쓰고 있어서 사용 중이라는 사실마저 쉽게 망각하는 리액트를 조금 딥하게 알아보기로 했다. 전문가를 위한 리액트 도서와 함께.


Before React

현재는 과거의 역사라고 하던가. 약 10년 간 리액트가 프런트엔드 시장에 끼친 지배적인 영향력은 아무것도 없는 환경에서 나왔을 리 없다. 문제가 있었고, 그 문제를 해결하는 여러 방식 중 하나가 리액트였다.

초창기 웹은 정적 페이지가 많았다. 사용자가 웹 사이트에서 할 거라곤 하이퍼링크를 클릭해서 새 페이지로 이동하거나 Input창에 검색어를 입력하는 폼 제출이 대부분이었다.

하지만 웹의 신세계를 맛본 이상 더 많은 아이디어가 밀려왔을 터. 다만 웹의 폭발적인 확장성을 뒷받침하기엔 3가지 문제가 있었다.

첫째, 성능
사용자가 좋아요 버튼을 누름으로써 아이콘 색상이 바뀌고 count가 하나 늘어난다고 하자. 앞서 말한 두 가지만 바뀌는 행위이지만, 페이지 단위로 업데이트가 일어났기 때문에 브라우저가 페이지 내의 모든 레이아웃을 다시 계산하고 그리는 리플로우 과정이 큰 범위로 발생했다. 페이지를 그려내기 전까지 병목 현상이 생길 수밖에 없었고, 그만큼 성능은 떨어졌다.

둘째, 신뢰성
웹 환경이 복잡해지면서 일관적인 상태 유지에 어려움이 생겼다. 같은 상태인데도 출처가 여럿이다보니 무엇 하나 바꾸는 데에 드는 리소스가 컸다. 게다가 하나의 코드 베이스를 공유하는 협업 상황에선 더 말할 것도 없다.

마지막 셋째, 보안
XSS나 CSRF 등의 웹 공격을 방지하기 위한 데이터 소독, 즉 santinize가 필수적이었다.

달리 말하면 리액트는 이 3가지 문제를 해결하면서 프런트엔드 웹 생태계의 주도권을 잡았다고 본다. 그렇다고 아무것도 없는 황무지에서 리액트가 띨롱 나온 것은 아닐 터. 문제를 해결하고자 했던 여러 시도들이 있었기 때문에 이를 기반으로 새로운 아이디어를 모을 수 있었다고 본다. 그럼 리액트 이전의 세계를 조금 더 살펴보자.



앞서 말했듯 웹의 세상이 열리며 사용자가 우르르 몰려오면서 이런저런 기능을 집어넣기 시작했고, 서서히 과부하도 누적됐다. 개발자가 일일히 DOM을 조작했기 때문이다.

예를 들어, 좋아요 버튼을 클릭했을 때 텍스트를 바꿔달라는 요청이 들어왔다고 하자.

button 태그에다가idclassName을 붙여준다. 그리고 버튼 클릭했을 때의 함수를 만든다: 해당 엘리멘트를 타겟팅하고, 이벤트 리스너를 추가해 텍스트를 바꾼다. 뭐 이정도까진 간단하다. 그런데 요구사항이 늘어난다. 단순 버튼 클릭이 아니고 토글이란다. 이미 좋아요 한 상태에서 다시 클릭하면 이전 상태로 되돌려야 한다.

이를 위해 별도의 상태 속성(data-liked)을 해당 버튼에 추가한다. 그리고 다시 돔 조작을 한다. data-liked의 속성이 true일 땐 false로, false일 땐 true로 바뀌게끔.

여기까지가 화면을 다루는 내용이고, 해당 데이터를 서버에 보내 DB에 저장해야 한다. 리액트 등장 전이니까 브라우저의 XMLHttpRequest 객체로 네트워크 통신이 이루어졌을 것이다. 서버 통신이 추가되면서 대기와 에러 상태, 즉 에지 케이스 처리도 필요하다. 이를 위한 추가 속성을 버튼에다 붙여준다.

<!-- HTML -->
<button id="like-button" class="like-btn" data-liked="false">
  좋아요
</button>
<span id="like-count">0</span>

<script>
  // 버튼, 카운트 요소 가져오기
  const likeButton = document.getElementById('like-button');
  const likeCount = document.getElementById('like-count');

  // 클릭 이벤트 등록
  likeButton.addEventListener('click', function() {
    // 현재 상태 확인
    const liked = likeButton.getAttribute('data-liked') === 'true';

    // 상태 토글
    likeButton.setAttribute('data-liked', !liked);

    // 버튼 텍스트 변경
    likeButton.textContent = liked ? '좋아요' : '좋아요 취소';

    // 카운트 증가/감소
    likeCount.textContent = liked
      ? parseInt(likeCount.textContent) - 1
      : parseInt(likeCount.textContent) + 1;

    // 서버에 상태 전송 (XHR)
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/like');
    xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
    xhr.onload = function() {
      if (xhr.status !== 200) {
        // 에러 처리
        alert('좋아요 요청에 실패했습니다.');
        // 원래 상태로 복구
        likeButton.setAttribute('data-liked', liked);
        likeButton.textContent = liked ? '좋아요' : '좋아요 취소';
        likeCount.textContent = liked
          ? parseInt(likeCount.textContent) + 1
          : parseInt(likeCount.textContent) - 1;
      }
    };
    xhr.send(JSON.stringify({ liked: !liked }));
  });
</script>

그런데 웹 페이지에 좋아요 버튼이 딱 하나일 리가 있나. 사용자가 클릭할 버튼이 여럿일 거다. 요구사항과 작업량, 복잡성이 비례하는 구조에서는 1) 오류 발생 가능성이 높고 2) 상태 예측이 어렵고 3) 성능이 비효율적이다. 소프트웨어 요구사항은 나날이 발전하는데 개발자가 일하는 환경은 주먹구구식이라니. 모든 개발자가 풀고 싶은 숙제이지 않았을까.

그렇게 자바스크립트 기반 솔루션들이 등장한다:


1. 제이쿼리

(최근 10년 만에 메이저 버전 업데이트가 된) 제이쿼리는 DOM 조작 수고스러움을 덜어준 존재였다. 당시 브라우저는 표준이랄 게 없어서 브라우저 호환성 문제가 있었다. 그래서 DOM API도 조금씩 다르다보니 브라우저 각각을 위한 별도 코드를 작성해야 했다.

document로 시작하는 셀렉트를 함수 $로 축약하고, 보다 코드를 간단하게 작성할 수 있었다. 자그마한 추상화가 이루어진 셈이다. 코드의 가독성 개선에도 도움되었지만, 어떤 브라우저에서든 똑같이 동작한다는 점이 가장 큰 개선점이었다.

하지만 문제의 본질을 해결했느냐고 하면 그건 아니었다. DOM을 직접 조작하는 방식은 동일했고, 편의성을 더해준 제이쿼리의 $()가 되려 발목을 붙잡았다. 어느 페이지에서든 호출하면 전역적으로 영향을 미칠 수 있었기 때문이다. 즉, 어떤 코드 때문에 바뀐 건지 디버깅하기가 점점 어려워졌다.

화면은 false 상태인데 실제 데이터는 true이거나 이벤트 순서에 따라 결과가 달라지거나 한 기능을 고치면 전혀 상관없어 보이던 다른 페이지가 영향을 받는 등 부작용이 심했다. 테스트하기도 쉽지 않았다. 제이쿼리는 브라우저에 강하게 의존한다. DOM이 있어야 동작했기에 로직만 똑 떼어서 검증하기가 쉽지 않았다.

여기에 또 다른 변화가 찾아온다. 브라우저 자체가 점점 똑똑해진 것이다. querySelector, classList, fetch 같은 네이티브 API들이 등장했고, 네이티브 자바스크립트 메서드도 지속적으로 개선되었다. 제이쿼리 메서드와 네이티브 메서드 간 성능 차이가 거의 없어진 상태에서 용량은 묵직한 라이브러리가 되어버린 셈이다.

그렇다고 해서 제이쿼리 사용을 지양해야 한다는 건 아니다. 브라우저 호환성 문제에 대한 해결 방식을 제안하고, UI 제어 진입 장벽을 낮추기도 했으니 말이다. 고로 온갖 솔루션이 쏟아진 상황에서 웹 성장의 중추 역할을 한 건 사실이다. 다만 규모가 커지고 요구사항이 복잡해지면서 DOM을 직접 조작하는 방식 자체가 한계에 부딪혔을 뿐이다.

그리고 이 시점에서 새로운 물음이 던져졌다:

무엇이 바뀌었는지를 기준으로
화면을 그릴 수는 없을까?


2. Backbone

2010년경 등장한 Backbone은 제이쿼리에서 반복된 문제들—브라우저와 자바스크립트 간 상태 불일치, 코드 재사용의 어려움, 까다로운 테스트 방법—에 대해 새로운 접근을 제시했다. 핵심은 DOM 조작 방식 변화가 아니라, 구조였다. '애플리케이션을 역할 단위로 나누자', 즉 MVC 패턴을 웹 개발에 적용했다.

MVC 패턴

Source: Gemini

모델 (Model)
데이터의 출처. 서버에서 받아온 값이든, 사용자의 입력이든 애플리케이션이 다루는 상태는 모델에 모인다.

뷰 (View)
모델에서 데이터를 받아 화면에 그리는 역할. DOM 조작은 이 영역에 국한된다.

컨트롤러 (Controller)
모델과 뷰 사이에서 둘을 연결하는 브릿지. 사용자 입력을 받아 모델을 업데이트하고, 그 결과를 뷰에 반영한다.

이로써 비즈니스 로직, 인터페이스, 입력이 각각 분리 되었다. 관심사가 분리는 다른 말로 하면 이전보다 모듈화 되었다는 것이기도 하다. 유지보수, 확장, 테스트가 수월해졌다.

그리고 Backbone은 이 패턴을 활용한 API를 제공했다. 모델과 뷰를 어떻게 정의하고 연결하면 되는지 정해준 셈이다. 예를 들어 버튼을 만들 때 인스턴스를 생성하고, render 메서드를 통해 화면에 그리는 식이다. 로직을 하나의 단위로 묶을 수 있다보니 DOM 없이도 개별적인 테스트가 가능했다.

이만큼 나아졌지만, 한계 역시 뚜렷했다.

우선 보일러플레이트가 장황했다. 모델 하나, 뷰 하나를 만들기 위해 작성해야 할 코드가 많았고, 작은 기능을 추가할 때도 구조부터 잡아야했다.

그리고 양방향 데이터 바인딩을 기본적으로 제공하지 않았다. 모델 데이터가 바뀐다고 해서 DOM이 자동으로 갱신되지도, 반대로 DOM 변경이 모델에 반영되지도 않았다. 결국 개발자가 직접 작성하거나 별도의 플러그인에 의존해야 했다.

Backbone은 이벤트 중심 아키텍처다. 데이터(모델)가 업데이트되면 이벤트가 발생하고, 이를 구독한 곳들이 반응한다. 결국 하나의 이벤트가 애플리케이션 전역으로 퍼질 수 있었다. 곧 영향의 시작과 범위를 추적하기가 점점 어려워졌다. 제이쿼리에서 발생한 문제점과 비슷한 형국이다.

마지막으로 조합성의 한계도 있었다. 뷰를 중첩해서 구성하는 기능이 내장되어 있지 않았기 때문에, UI가 복잡해질수록 구조를 직접 관리해야 했다. 작은 앱에서는 감당할 수 있었지만, 규모가 커질수록 부담일 수밖에 없다.

이 시기 Backbone과 비슷한 시기에 등장한 솔루션이 있는데, 문제를 풀어간 초점이 조금 달랐다.

3. Knockout

상태가 바뀔 때마다
화면도 자동으로 그릴 수 있다면?

Backbone이 관심사 분리에 집중했다면, Knockout은 상태와 화면을 연결하는 방식에 초점을 맞췄다. MVC의 모델 역할 격인 옵저버블(observable)과 뷰 역할을 하는 데이터 바인딩(binding) 개념을 통해서 말이다.

Knockout은 상태를 옵저버블로 감싸서 이 상태를 사용하는 뷰를 자동으로 추적한다. 그래서 데이터가 바뀌면 그 데이터를 참조하는 UI가 바로 다시 그려진다. 개발자가 DOM 업데이트를 직접 할 필요가 없다. 자바스크립트 생태계 최초의 반응형(Reactive)이었다. 이 접근은 MVC보다 MVVM(Model–View–ViewModel) 패턴과 맞닿아 있었다.

MVVM(Model–View–ViewModel) 패턴

모델 (Model)
애플리케이션의 데이터와 비즈니스 로직. 순수한 상태의 집합이다.

뷰 (View)
사용자에게 정보를 보여주고 입력을 받는 역할. MVC에서와 달리 수동적이다. 상태를 직접 변경하지 않고, 바인딩된 값을 그대로 반영한다.

뷰 모델 (ViewModel)
모델과 뷰 사이의 브릿지. 모델의 데이터를 뷰에서 쓰기 좋은 형태로 가공하고, 옵저버블로 노출한다. 이곳에 프레젠테이션 로직이 모인다.

이 구조 덕분에 몇 가지 장점이 분명해졌다. 먼저 상태 변경에 맞춰 화면 갱신이 자동화 되었고, 테스트를 뷰 모델 단위로 진행할 수 있다. 프레젠테이션 로직을 분리했으니 재사용도 가능해지고 말이다. Backbone에 비해 보일러플레이트 코드가 줄었다는 점도 그렇다.

그러나 Knockout도 데이터 흐름 추적이 어려운 건 매한가지였다. 뷰에서의 변경이 곧바로 모델에 반영되고, 그 결과가 다시 뷰로 전파되다보니 어느 지점에서 상태가 바뀌었는지, 그 변경이 어디까지 영향을 미치는지 파악하기 쉽지 않았다.

앞선 MVC 패턴과 MVVM 패턴을 비교하자면 이렇다:

구분MVCMVVM
주요 목적관심사 분리로 애플리케이션 구조화상태 변화에 따라 UI를 자동으로 동기화
구성 요소Model / View / ControllerModel / View / ViewModel
데이터 흐름명시적 호출 중심바인딩 중심
결합 방식Controller가 View와 Model을 연결하는 명시적 방식View와 Model은 ViewModel을 통해 간접 연결 (암묵적)
사용자 상호 작용입력 → Controller → Model입력 → ViewModel → View (바인딩)
적합한 환경비교적 단순한 UI, 명확한 데이터 흐름상태 변화가 잦고 동적인 UI

큰 틀은 비슷하나 결합과 데이터 바인딩을 정의하는 관점이 다르다.

자, 그럼 여기까지 보았듯 모든 프레임워크/라이브러리에는 한계가 명확한 만큼 장점도 뚜렷하다. 문제에 대한 정의가 조금씩 달라서 해결 방식의 차이가 생길 뿐. 그럼 그 관점을 합친다면 어떨까.

역할 분리가 명확한 아키텍처에서
상태 변경에 맞춰 화면 갱신도 자동으로 된다면?

이 질문에 대한 답으로 AngularJS가 등장한다.

앵귤러JS

앵귤러의 전신(前身). 구글이 만들었고, 엔터프라이즈급 SPA를 목표로 한 프레임워크다.

AngularJS는 모델과 뷰를 양방향 데이터 바인딩으로 연결했다. 데이터가 바뀌면 화면이 자동으로 갱신되고, 화면에서의 입력은 다시 모델에 반영된다. 개발자가 상태 변경과 DOM 업데이트를 일일이 관리하지 않아도 되도록 설계된 구조였다.

여기에 의존성 주입을 기본 개념으로 삼아 컴포넌트 간 결합도를 낮추고, 템플릿 기반의 선언적 UI를 통해 HTML 중심의 개발 경험을 제공했다. 라우팅, 폼 처리, HTTP 통신 등 애플리케이션에 필요한 대부분의 기능을 프레임워크 내부에서 해결할 수 있도록 한 점도 특징이다. 이 덕분에 프런트엔드에서도 규모 있는 애플리케이션을 만들 수 있다는 인식이 자리 잡게 된다.

다만 이 편의성은 복잡성으로 이어졌다. 러닝 커브가 높았고, 많은 동작이 프레임워크 내부에서 자동으로 처리되다 보니 실제로 무슨 일이 일어나는지 파악하기 어려웠다. 특히 digest cycle 기반의 변경 감지는 애플리케이션이 커질수록 성능 병목으로 이어지기 쉬웠다. 양방향 데이터 바인딩 역시 규모가 커질수록 데이터 흐름을 추적하기 어렵게 만들었다.

여기에 마침표를 찍은 건 Angular 2+로의 급격한 전환 아니었을까 싶다. 커뮤니티 생태계는 사용자들이 만들어 가는 것인데 그 사용자들에게 완전히 이탈할 통로를 활짝 열어줬으니 말이다.

이쯤에서 정리해보면 이렇다.

제이쿼리는 DOM을 쉽게 다루는 방법을 제시했고, Backbone은 MVC 패턴을 도입해 아키텍처 구조화를 시도했다. Knockout은 상태가 바뀔 때마다 화면이 자동으로 반영되는 방식을 도입했고, AngularJS는 이 모든 것을 하나의 프레임워크 안에 통합하고자 했다.

React: 패러다임의 변화


이 시도들이 마주한 문제도 고질적이다: 상태 소유권과 흐름 통제. 같은 데이터가 여러 곳에서 변경되고, 이벤트 순서에 따라 결과가 달라지며, 변경의 시작점을 추적하기 어려운 상황이 반복되었다.

React는 관점의 방향을 바꿨다. 상태를 여러 방향으로 흘려보내는 대신, 한 방향으로만 흐르게 하는 발상으로. 상태의 원천을 명확히 규명한다. 데이터는 부모에서 자식으로만 전달되고, 컴포넌트는 자신에게 필요한 상태만을 소유한다. 이 단순한 규칙 덕분에 데이터가 어디서 시작되어 어디까지 영향을 미치는지 추적하기 쉬워졌다.

또 하나의 변화는 컴포넌트를 바라보는 관점이다. MVC처럼 기능별로 코드를 나누는 대신, UI 단위로 데이터와 로직을 묶는다. 화면을 구성하는 조각 하나하나가 컴포넌트가 되고, 그 안에 필요한 상태와 동작이 함께 들어간다. 이 방식은 컴포넌트 단위의 캡슐화를 가능하게 했고, 애플리케이션을 조립하듯 구성할 수 있게 만들었다.

단방향 흐름은 아키텍처 차원에서 Flux 패턴으로 명명한다.

Source: Gemini

Flux에서는 사용자의 행동이 하나의 액션으로 정의되고, 이 액션이 디스패처를 통해 스토어로 전달된다. 스토어는 상태를 변경하고, 그 결과를 다시 뷰가 읽어 렌더링한다. 이 과정은 항상 같은 방향으로만 흐르기 때문에, 문제가 생기면 위로 거슬러 올라가며 원인을 추적할 수 있다. 페이스북에서 발생했던 ‘읽지 않은 알림’ 버그 사례는 이런 구조가 왜 필요한지를 잘 보여준다.

React가 제시한 또 하나의 중요한 개념은 선언적 UI다. 개발자는 DOM을 어떻게 조작할지 고민하는 대신, 특정 상태에서 화면이 어떤 모습이어야 하는지만 정의하면 된다. 가상 DOM은 이 선언을 가능하게 만든 장치다. 실제 DOM과 가상 DOM을 비교해 변경된 부분만 갱신함으로써 성능을 개선했고, 사용자 경험 역시 자연스러워졌다. 무엇보다 개발자는 DOM 조작이라는 저수준 작업에서 해방될 수 있었다.


짧은 소견

리액트가 왜 탄생했는지를 이해해야 비로소 '리액트는 선언적으로 작성해야 한다'는 말의 의미가 선명해지는 것 같다. 가독성과 편의성은 부차적인 효과다. 라이브러리는 단순히 개발자의 일을 줄여준 도구에 그치지 않는다. DOM 조작 업무를 라이브러리에 위임했음을 인지하고 있어야 왜 이렇게 동작하는지를 이해하게 된다. 알아보려고 할 수록 의도대로 구현할 가능성이 높아지고 말이다.

이게 '인간은 도구를 사용하는 동물이다'는 뜻이지 않을까.

Source: Gemini
profile
Hello, Developers!

0개의 댓글