Dev Thinking
32완료

자바스크립트와 리액트 카운터로 보는 첫 차이

2025-08-01
8분 읽기

리액트 공식문서 기반의 리액트 입문기

들어가며

리액트가 처음 등장한 지 10년이 넘었지만, 여전히 프론트엔드 개발 트렌드의 대표적인 주자로 굳건히 자리를 차지하고 있습니다. 이러한 굳건함의 배경에는 UI를 더욱 효율적이고 예측 가능하게 구축하려는 개발자들의 끊임없는 노력이 있었고, 리액트는 이러한 요구에 부응하며 선언적 UI 라이브러리의 대표 주자로 자리매김했습니다.

저는 그동안 바닐라 자바스크립트로 개발해왔지만, 시대의 흐름 속에서 뒤늦게 리액트 학습의 필요성을 느끼게 되었습니다. 이 시리즈는 제가 리액트 공식 페이지의 '학습하기'(이하 공식문서)를 탐독하며 얻은 지식과 학습 과정을 정리하는 글이 될 것입니다. 이 여정이 저와 비슷한 상황에 있는 다른 개발자들에게도 작은 도움이 되기를 바랍니다. 앞으로 글 속에서 '저'나 '우리'라는 표현이 나올 때면, 바로 여러분을 염두에 둔 지칭이라고 이해해 주시면 좋겠습니다.

이 시리즈는 공식문서서의 내용을 바탕으로 제가 학습하며 느낀 부분들을 보완하여 구성했습니다. 시리즈 초~중반부에서는 바닐라 자바스크립트와 리액트의 기능을 비교하며 설명하고, 후반부에서는 리액트 고유 기능에 집중하여 다룹니다.

첫 번째 포스트인 1-1편에서는 가장 기본적인 '카운터' 예제를 통해 우리가 익숙한 자바스크립트와 리액트가 어떤 핵심적인 차이점을 가지는지 살펴보며 이 여정을 시작하겠습니다.

자바스크립트로 카운터 만들기

먼저 우리가 늘 사용해온 방식으로 카운터 기능을 구현해 보겠습니다. HTML 문서에 버튼과 숫자를 표시할 요소를 만들고, 자바스크립트를 이용해 버튼 클릭 시 숫자가 증가하도록 구현하는 것이죠. 다음 코드를 직접 실행해 보면서 어떻게 작동하는지 확인해 볼 수 있습니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla JS Counter</title>
    <!-- 스타일 코드는 생략합니다. 핵심 동작만 보여줍니다. -->
  </head>
  <body>
    <h1>자바스크립트 카운터</h1>
    <div id="counter-display">0</div>
    <button id="increment-button">증가</button>
 
    <script>
      (function () {
        let count = 0;
        const counterDisplay = document.getElementById("counter-display");
        const incrementButton = document.getElementById("increment-button");
 
        if (incrementButton && counterDisplay) {
          incrementButton.addEventListener("click", () => {
            count = count + 1;
            counterDisplay.innerText = count;
          });
        }
      })();
    </script>
  </body>
</html>

위 코드를 보면, 우리는 count라는 변수를 즉시 실행 함수(IIFE) 내부에 선언하여 전역 스코프 오염을 방지하고, 버튼 클릭 시 이 변수의 값을 1 증가시킨 다음, counterDisplay.innerText를 직접 조작하여 화면에 표시된 숫자를 업데이트했습니다. 이는 우리가 오랫동안 익숙해왔던 방식이죠. 다음으로 자바스크립트 방식의 특징을 살펴보겠습니다.

자바스크립트 방식의 특징

  1. HTML 먼저, JavaScript 나중: HTML 요소를 먼저 정의하고, 자바스크립트트 코드에서 document.getElementById와 같은 DOM API를 사용하여 해당 요소를 선택하고 조작합니다. UI의 구조와 동작 로직이 분리되어 작성됩니다.
  2. 명령형 DOM 조작: innerTextstyle 속성을 직접 변경하는 것처럼, UI를 업데이트하기 위해 DOM 요소에 어떤 변화를 가해야 할지 개발자가 직접 '명령'하는 방식으로 코드를 작성합니다. 즉, '어떻게(How)' UI를 변경할지 상세히 지시합니다.
  3. 이벤트 리스너 직접 등록: addEventListener 메서드를 사용하여 특정 DOM 요소에 이벤트 핸들러를 직접 등록합니다. 이벤트 발생 시 실행될 콜백 함수를 명시적으로 연결해야 합니다.
  4. 수동적인 UI 업데이트: 데이터(count 변수)가 변경되더라도 화면(UI)은 자동으로 업데이트되지 않습니다. 개발자가 직접 counterDisplay.innerText = count;와 같은 코드를 통해 변경된 데이터를 UI에 반영해야 합니다.
  5. 로컬 스코프 변수와 수동적 상태 관리: 위 예제에서 count 변수는 즉시 실행 함수 내부에 선언되어 전역 스코프 오염을 피했습니다. 하지만 여전히 개발자가 변수의 값을 직접 변경하고, 변경된 값을 DOM에 수동으로 반영해야 합니다. 이는 애플리케이션 규모가 커질수록 상태 관리의 복잡성을 증가시킬 수 있습니다.

리액트로 동일한 카운터 만들기

이제 리액트 방식으로 동일한 카운터 기능을 만들어 보겠습니다. 리액트는 UI를 구성하는 방식과 상태를 관리하는 방식에서 자바스크립트와 큰 차이를 보입니다. 특히 '컴포넌트'라는 개념과 '상태(State)'를 활용하여 UI를 더욱 선언적으로 만들 수 있습니다. 여기서는 리액트를 처음 접하는 분들을 위해 이 포스트 시리즈에서 처음이자 마지막으로 React.createElement를 사용한 버전과 앞으로 우리가 실제 개발에서 더 많이 보게 될 JSX 버전을 모두 보여드리겠습니다.

React.createElement 버전

이 코드는 리액트 개발팀에서 레거시로 분류되었지만, JSX 없이 순수 자바스크립트로 리액트 요소를 생성하는 방법을 보여주며, 리액트가 내부적으로 어떻게 작동하는지 이해하는 데 도움이 됩니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React.createElement Counter</title>
    <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <!-- 스타일 코드는 생략합니다. 핵심 동작만 보여줍니다. -->
  </head>
  <body>
    <h1>React.createElement 카운터</h1>
    <div id="root"></div>
 
    <script type="text/javascript">
      const useState = React.useState;
      const root = ReactDOM.createRoot(document.getElementById("root"));
 
      function Counter() {
        const [count, setCount] = useState(0);
 
        const handleClick = () => {
          setCount(count + 1);
        };
 
        return React.createElement(
          "div",
          null,
          React.createElement("div", { id: "counter-display" }, count),
          React.createElement("button", { onClick: handleClick }, "증가")
        );
      }
 
      root.render(React.createElement(Counter));
    </script>
  </body>
</html>

JSX 버전

실제 리액트 개발에서는 JSX라는 문법 확장 기능을 사용하여 더욱 직관적으로 UI를 작성합니다. JSX는 자바스크립트 안에서 HTML과 유사한 마크업을 작성할 수 있게 해주며, 빌드 과정에서 React.createElement 호출로 변환됩니다.

// App.jsx (또는 Counter.jsx)
import { useState } from 'react';
import ReactDOM from 'react-dom/client';
 
// 스타일은 생략. 필요시 CSS 파일 또는 styled-components 사용
 
function Counter() {
  const [count, setCount] = useState(0); // count 상태와 업데이트 함수 선언
 
  const handleClick = () => {
    setCount(count + 1); // setCount를 호출하여 count 값을 1 증가
  };
 
  return (
    <div>
      <div id="counter-display">{count}</div>
      <button onClick={handleClick}>증가</button>
    </div>
  );
}
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);

코드 설명: JSX와 React.createElement의 관계

위 두 리액트 코드 예제는 동일한 카운터 기능을 구현하지만, UI를 정의하는 방식에서 큰 차이를 보입니다. React.createElement 버전은 리액트가 내부적으로 UI 요소를 어떻게 구성하는지 이해할 때 사용합니다. React.createElement('태그', {속성}, '자식 요소') 형태로 작성되며, 매우 장황하고 복잡한 구조를 가질 수 있습니다. 이는 리액트 요소(React Element) 객체를 생성하는 함수입니다.

JSX: JavaScript XML의 약자로, 자바스크립트트 안에서 HTML과 유사한 마크업을 작성할 수 있게 해주는 자바스크립트트의 문법 확장입니다. JSX 코드는 빌드 시 Babel과 같은 트랜스파일러에 의해 React.createElement 호출로 변환됩니다. 따라서 JSX는 개발 편의성을 위한 문법적 설탕(Syntactic Sugar)이라고 할 수 있습니다.

왜 다르게 작성할까요? React.createElement는 리액트의 동작 방식을 이해하는 데 중요하지만, 코드가 복잡해질수록 작성 및 유지보수가 어렵습니다. 반면 JSX는 HTML과 유사하여 훨씬 직관적이고 가독성이 좋습니다. 대부분의 리액트 개발 환경에서는 빌드 도구를 통해 JSX가 자동으로 React.createElement 호출로 변환되므로, 개발자는 JSX를 사용하여 선언적으로 UI를 구성하는 데 집중할 수 있습니다.

특히 useState라는 리액트 Hook(훅)을 사용하여 count라는 '상태'를 관리하고, 이 상태가 변경되면 리액트가 알아서 UI를 다시 그려준다는 점이 핵심입니다. 다음으로 리액트 방식의 특징을 살펴보겠습니다.

리액트 방식의 특징

  • 1. 컴포넌트 중심 개발과 UI 모듈화: 리액트는 UICounter와 같은 독립적이고 재사용 가능한 컴포넌트 단위로 분리하여 개발하는 패러다임을 제시합니다. 각 컴포넌트는 자신만의 로직(useState 훅)과 UI(JSX)를 캡슐화하므로, 복잡한 UI를 관리 가능한 작은 단위로 분해하고, 이를 마치 레고 블록처럼 조립하여 전체 애플리케이션을 구축할 수 있게 됩니다. 이는 코드의 모듈화 수준을 높이고, UI의 변경이 다른 부분에 미치는 영향을 최소화하여 대규모 애플리케이션의 유지보수성을 크게 향상시키는 리액트의 핵심 강점입니다.
  • 2. 선언적 UI 정의와 개발자의 사고 전환: 리액트에서 개발자는 JSX를 사용하여 UI가 '어떻게 생겨야 하는지(What)'를 **선언적(Declarative)**으로 기술합니다. 즉, UI의 최종적인 상태와 모습을 정의하면, 리액트가 이 상태를 실제 DOM에 반영하는 '방법(How)'을 자동으로 처리합니다. 이는 자바스크립트에서 DOM을 직접 조작하며 모든 단계를 명령해야 했던 명령형(Imperative) 방식과 대비되며, 개발자가 UI 구현의 세부 사항보다는 애플리케이션의 핵심 비즈니스 로직과 UI의 의도에 더 집중할 수 있게 하여 개발 생산성과 코드의 예측 가능성을 높입니다.
  • 3. 상태 기반 자동 렌더링과 useState 훅의 역할: useState 훅을 사용하여 count와 같은 컴포넌트 내부의 **상태(State)**를 관리하는 것이 리액트의 핵심입니다. setCount와 같은 상태 업데이트 함수를 호출하면, 리액트가 변경된 상태를 감지하고 자동으로 해당 컴포넌트를 다시 렌더링하여 UI를 업데이트합니다. 개발자가 DOM을 직접 조작하며 UI와 데이터를 수동으로 동기화할 필요가 없어지므로, UI 버그 발생 가능성을 줄이고 개발자가 '상태 정의'와 'UI 묘사'에만 집중할 수 있게 하여 개발 생산성을 크게 향상시킵니다.
  • 4. 간결한 이벤트 핸들러와 UI/로직의 응집: JSX 문법 내에서 onClick={handleClick}와 같이 HTML 속성처럼 이벤트 핸들러를 연결하는 방식은 바닐라 자바스크립트의 addEventListener보다 훨씬 간결하고 직관적입니다. 이는 UI 마크업과 해당 UI의 동작 로직을 하나의 컴포넌트 안에서 함께 위치하게 하여 코드의 **응집도(Colocation)**를 높여줍니다. 개발자는 button을 클릭했을 때 '무엇을 할지'(handleClick)만 선언하고, '어떻게' 이벤트가 연결되고 실행될지는 리액트가 관리하므로, 코드의 가독성이 향상되고 개발자가 UI 로직에 더 집중할 수 있게 됩니다.
  • 5. Virtual DOM을 통한 효율적인 UI 업데이트: 리액트는 직접 DOM을 조작하는 대신, **Virtual DOM(가상 DOM)**이라는 메모리상의 가벼운 UI 복사본을 사용하여 효율적으로 UI를 업데이트합니다. 컴포넌트의 상태가 변경되면 리액트는 새로운 Virtual DOM 트리를 생성하고, 이전 Virtual DOM 트리와 비교(재조정 과정)하여 실제 DOM에서 변경이 필요한 최소한의 부분만을 찾아냅니다. 이 과정은 브라우저의 DOM 조작 횟수를 최소화하여 애플리케이션의 렌더링 성능을 최적화하는 핵심적인 메커니즘이며, 개발자가 DOM 성능 최적화에 대한 깊은 지식 없이도 고성능 UI를 구축할 수 있게 돕습니다.

명령형 vs 선언형 차이

자바스크립트와 리액트의 가장 근본적인 차이는 프로그래밍 패러다임에 있습니다. 자바스크립트의 DOM 조작은 주로 명령형(Imperative) 방식인 반면, 리액트는 선언형(Declarative) 방식에 가깝습니다.

  • 명령형(Imperative) 프로그래밍: '어떻게(How)' 작업을 수행할지 하나하나 지시하는 방식입니다. 예를 들어, "버튼을 찾아, 클릭 이벤트를 걸고, 숫자를 1 증가시킨 다음, 그 숫자를 화면에 다시 써라"와 같이 구체적인 단계를 명령합니다.
  • 선언형(Declarative) 프로그래밍: '무엇(What)'을 원하는지 설명하는 방식입니다. "카운터 숫자가 얼마일 때, 화면은 이렇게 보여야 한다"고 정의하고, 나머지 과정은 시스템(리액트)에 맡깁니다.

이 둘의 차이를 비교표로 더 명확하게 살펴보겠습니다.

구분자바스크립트 (명령형)리액트 (선언형)
상태 정의let count = 0;와 같이 일반 변수로 정의const [count, setCount] = useState(0); Hook으로 정의
상태 변경count = count + 1;와 같이 직접 변수 값 변경setCount(prevCount => prevCount + 1);와 같이 함수형 업데이트 호출
UI 업데이트element.innerText = count;처럼 DOM을 직접 조작상태 변경 시 리액트가 Virtual DOM을 통해 자동으로 업데이트
동기화 책임개발자가 데이터와 UI의 동기화를 수동으로 처리리액트가 데이터와 UI의 동기화를 자동으로 관리

요약

이번 1-1편에서는 자바스크립트와 리액트를 이용한 '카운터' 예제를 직접 구현해 보면서, 두 방식이 UI를 구성하는 데 있어 어떤 근본적인 차이점을 가지는지 탐구해 보았습니다. 자바스크립트가 DOM을 직접 조작하여 UI를 '어떻게' 변경할지 상세하게 지시하는 명령형 방식이라면, 리액트는 useState Hook을 활용하여 UI가 '무엇'을 보여줄지 선언하고 상태 변경에 따라 UI가 자동으로 업데이트되도록 하는 선언형 방식이라는 것을 이해하게 되었습니다. 또한, 리액트가 효율적인 UI 업데이트를 수행하는 핵심 메커니즘인 Virtual DOM에 대해서도 살펴보았는데요. 이번 편에서 제가 학습한 주요 특징들을 종합하면 다음과 같습니다.

  • 패러다임 차이: 자바스크립트의 명령형 DOM 조작과 리액트의 선언형 상태 기반 렌더링
  • 상태 관리: 자바스크립트의 수동적인 변수 관리와 리액트 useState Hook을 통한 자동 상태 관리
  • UI 업데이트: 자바스크립트의 직접적인 DOM 조작과 리액트 Virtual DOM을 통한 효율적인 UI 업데이트
  • 컴포넌트 중심 개발: UI를 독립적인 컴포넌트로 분리, UI 모듈화 및 재사용성 향상

다음 편(1-2편)에서는 개발 환경 설정을 시작으로, Next.js와 Vite를 비교하며 모던 툴체인을 살펴보겠습니다.

참고문서