[Java 풀스택 개발자] React의 언어체계

부트캠프 일지/멀티캠퍼스 TIL
2026.01.26

 

0. 들어가기에 앞서

본래는 React 실전으로 포스트를 작성하려고 했지만, React의 언어 체계에 대한 이론적 정리가 필요할 거 같아 계획을 변경했다. 대체 이 라이브러리는 무엇이길래 이렇게 복잡해보이면서도 쉬울까? 그 언어 체계에 대해서 파헤칠 필요가 있다고 생각해, 이번 포스트는 React의 언어체계에 대해서 다룬다. 본래라면 특별편으로 다뤄야하지만, 이번 주 학습에서 React의 분량이 적고, 처음 배우는 라이브러리이기에 이번에는 특별히 주차로서 다룬다.
 
 

1. React란

2013년 5월에 메타(Meta)에서 개발하여 출시한 JS 라이브러리이다. 출시 직후부터 큰 관심을 받아 개인 개발자들 사이에서는 자주 사용되고 있다. 회사에서도 신규 프로젝트에는 리액트를 사용하기 시작하여, 현재는 레거시(Vanilla JS 혹은 jQuery)와 혼용되고 있다. 그 덕분에 아직까지도 프론트엔드에서 사용되는 라이브러리 중 상위권을 유지하고 있는데, 최근에는 Next.js 같은 프레임워크가 떠오르고 있다. 서버 사이드 렌더링 방식이 선호되기 시작했기 때문에, 순수 React의 사용보다는 이러한 프레임워크가 더 주목받는 추세다.
 

 
 
물론 아직까지 기존 언어들도 혼용으로 사용되고 있다. 일부 기술의 경우 Vanilla JS로만 이루어지기도 하기 때문에, React만을 사용해서는 JavaScript를 정복할 수 없다고 할 수 있다. 하지만 그 언어체계가 레거시와는 다르기 때문에, React가 개발 직후부터 지금까지 꾸준히 러브콜을 받고 있을 것이다. 대체 무엇이 React를 특별하게 만드는 걸까? 이 포스팅에서는 렌더링 방식, JSX라는 문법, 그리고 컴포넌트 구조, SPA를 포함한 React의 개성을 알아보고자 한다.
 
 

2. 렌더링 방식

React는 선언적 렌더링 방식을 사용하고 있다. 일반적인 JavaScript는 DOM을 직접 조작하는 명령형 방식이다. 명령형과 선언형의 차이에 대해서 이야기한다면, 순서에 차이가 있다. JS는 일반적으로 DOM을 먼저 만든 뒤 그 DOM에 지시하여 원하는 동작을 실행한다. 그러나 React의 경우, 먼저 DOM에 어떤 동작을 실행할지 선언한 뒤, 해당 DOM을 만들어낸다.
비유하자면 JS 방식은 주문을 받아 옷감(DOM)을 재단 및 수선하여 옷을 내놓는 것이고, React의 경우 이미 만들어진 기성복을 판매하는 것과 같다. 따라서 손님도 편하게 옷을 살 수 있지만(=속도 및 성능이 기존의 JS보다 좋다), 단점도 당연히 있다.
 

 
 
선언형은 작은 프로젝트에서는 부적절하다고 할 수 있는데, 이유는 간단하다. React를 위해서는 기본적으로 셋업이 되어야하는 것들이 있다. 그리고 수많은 컴포넌트를 컨트롤하여 DOM을 생성해야하는데, 동작이 그만큼 많지 않으면 단순한 HTML 문서에 기존의 JS를 선언하는 것이 낫다. 코드를 수정하거나 커스텀할 때의 번거로움도 실질적으로는 기존보다 복잡하다. 그러므로 큰 프로젝트에서 주로 사용된다.
 
이러한 렌더링 방식은 다음 3단계로 정리할 수 있다.
 

1.렌더링 트리거 → 2. 컴포넌트 렌더링 → 3. DOM에 커밋

 
렌더링이 될 때는 1. 최초 한 번, 그리고 2. State의 변경 당시가 있는데, 이것이 렌더링 트리거가 된다. state에 대해서는 후에 이야기하겠다. 그리고 이 트리거를 읽은 React는 어떤 컴포넌트를 렌더링할 것인가 확인해 호출한다. 호출하는 컴포넌트의 경우 1. 최초 한 번이라면 Root를, 2. State의 변경(업데이트)라면 업데이트가 일어난 컴포넌트를 호출한다. 그렇게 호출한 뒤에는 DOM에 변경사항을 커밋한다. 이 3단계에서 우리(클라이언트)의 눈에 보이게 된다. 만약 여기서 렌더링 결과가 이전과 같으면 React는 DOM을 건드리지 않고, 변화는 없게 된다.
 
이런 렌더링 방식을 보면 의아할 것이다. 어떻게 React는 렌더링을 할 수 있을까? 그 중심이 되는 게 바로 JSX이다.
 
 

3. 문법의 특이성: JSX

JSX는 JavaScript를 확장한 문법으로, JS를 HTML과 비슷하게 마크업을 작성할 수 있도록 해준다. 물론 React에서는 기존 JS 문법도 지원하기도 한다. JSX와 React는 굳이 따지면 별개의 것이다. 그러나 JSX는 하나의 마크업 언어와도 같으며, 마크업 언어를 쓰는 이유가 간편하기 위해서라면 굳이 JSX를 쓰지 않고 React를 사용할 이유는 없다.
 
JSX의 이해를 쉽게 하기 위해 예시를 적어보자. 여기 JS 코드가 있다.

const root = document.getElementById('root');

function render() {
  const title = document.createElement('h1');
  title.textContent = 'Hello, world';
  root.innerHTML = '';
  root.appendChild(title);
}

render();

 
보면 root라는 id를 가진 요소가 있다. 그리고 function render에서 title이라는 변수명을 가진 h1 요소를 생성한다. 그리고 내부에 Hello, world라는 text를 넣고, root라는 id를 가진 요소 안에 넣는다. render()를 호출하여 그 과정을 실행한다.
 
그러면 같은 로직을 가진 JSX 사용 React 코드이다.

import React from 'react';
import ReactDOM from 'react-dom/client';

function App() {
  return <h1>Hello, world</h1>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

 
 
import의 경우 React와 ReactDOM을 가져온다. 
그리고 보다시피, function App()에서 Hello,world라는 h1요소를 반환한다고 이야기한다. 그리고 root를 만들어, 그 안에 렌더한다.
 

 
이렇게 JSX를 사용하면 이 컴포넌트가 어떤 UI를 그리는지를 함수 안에서 한 번에 볼 수 있다. 따라서 코드를 하나하나 추적하지 않아도 화면 구조를 직관적으로 이해할 수 있게 되고, 그렇게 만드는 것이 가능하다.
 
 
 

4. 컴포넌트 기반 구조

React의 철학은 재사용과 독립성이라고 할 수 있다. 이것이 실질적인 React의 개발 이유가 된다. 특히 위에서 보았듯이 React가 굳이 DOM을 만들어 내놓는 까닭은, 이 DOM을 재활용할 수 있는가에 대해서 생각했기 때문이다. 후에 다룰 state를 살펴볼 때마다 든 생각은, 굳이 이렇게 번거로워야 할까? 였다. state의 경우 생각보다 로직이 복잡하고, 어쨌든 간에 렌더링이라는 과정을 또 거쳐야 한다. 그런 지점은 명령형인 기존 JS가 낫다. 그럼에도 React가 이런 형태를 고집한 건 이 재사용과 독립성 때문이다.
이전 JS가 한 덩어리로 굳어 있는 콘크리트 성이라면, React로 만든 페이지는 하나의 레고블록으로 쌓은 성과 같다. 레고블록은 분리해서 다른 위치에 붙일 수 있다. 반면 콘크리트 성은 분리할 수 없고, 따라서 일부를 재사용하는 것도 어렵다.
 

 
 

Meta는 어째서 컴포넌트를 만들었는가

여기서 우리는 이 React를 만든 회사가 메타(구 페이스북)라는 점을 떠올려보면 이해하기 쉬워진다. 우리가 페이스북의 메인 페이지에 접속했다고 가정하자. 상단에는 끊임없이 숫자가 변하는 알림 배지가 있고, 중앙에는 수만 명의 사용자가 실시간으로 올리는 뉴스피드가 흐르며, 오른쪽 하단에는 친구의 접속 상태를 알려주는 채팅창이 떠 있다.
 
기존 방식대로라면 알림이 하나 올 때마다 브라우저는 어느 위치의 어떤 태그를 바꿀지 일일이 계산해서 명령을 내려야 할 것이다. 이 부분이 중요하다. 명령형의 가장 큰 문제는, 동작이 많아질 수록 함수가 늘어나고, 그럴 때마다 변수의 관리가 어려워진다. 아무리 블록 스코프를 사용한다고 할지언정, 한 페이지에서 이렇게 많은 행동을 명령시키다보면 개발자 입장에서도 코드의 파악이 어렵다. 그러다보면 결국에는 버그가 어떻게 일어날지 알 수가 없다.
그러나 React는 다르다. 각 컴포넌트가 독립적이고, 구별된다. 서로를 의도적으로 간섭하지 않는 한, 간섭하지 않는 것이 원칙이다. 알림 배지, 채팅창, 뉴스피드는 서로에게 간섭하지 않는다. 이따금 만들어둔 컴포넌트를 사용하여 UI를 공유할 때도 있겠지만, 그뿐이다.
 
그러므로 이런 컴포넌트 기반 구조는 SPA(Single Page Application)와도 깊게 관련이 있다. 어쩌면 스마트폰과 함께 SNS(Social Networking Service)라는 존재가 탄생하면서, React의 탄생도 예견 되어있었을지도 모른다.
 

이 모든 걸 한 페이지에 담아야 한다고? 명령형으로는 도저히 못한다.

 
 
 

5. SPA와 React

앞서 언급했듯, React의 컴포넌트 기반 구조는 SNS라는 거대하고 복잡한 서비스를 관리하기 위해 탄생했다. 그런데 여기서 생각해볼 것이 있다.
 

아니, 결국 이 모든 건 한 페이지내에 해결해야해서 생긴 문제잖아?
여러 페이지라면 이렇게까지 복잡한 구조를 가질 필요가 있나?

 
 
그렇다. 이 모든 건 한 페이지라 생긴 문제다. 그리고 그 한 페이지만을 위한 세레나데인 React는 고스란히 멀티 페이지에서는 그 특성이 심각하게 침해받는다. 아무리 컴포넌트를 잘게 쪼개고 독립적으로 만들어봤자, 사용자가 다른 페이지로 이동할 때마다 브라우저가 새로고침을 해버린다면 의미가 없다. 이를 위한 React의 대표 개념이 바로 SPA(Single Page Application)다.
 
React 이전부터 SPA에 대한 열망과 시도는 있어왔다. 기존 방식인 MPA가 가진 치명적인 단점은, 페이지간 데이터 이관이 매우 불편하다는 지점이다. 거기에 괜한 새로고침으로 인한 하얀 화면에 괜히 기분 나쁨을 느끼는 고객들은 덤이다. 그러하여 많은 개발자들이 이런 한계를 극복하고자 했다. ajax라는 비동기식 통신이 이에 대한 적절한 예제가 될 것이다. 그러나 SPA는 한 페이지에 많은 것들을 해결해야 한다는 무지막지한 요구에, 그동안 불가능을 선고받았던 것이다.
그것을 React는 극복하고자 했다. 정확히는 메타가 페이스북의 서비스를 늘리며, MPA 방식에 한계를 느껴 만들어낸 것이 React이다. MPA 방식을 피하려면 그동안 SPA가 가지고 있던 숙제를 해결해야만 했다. 그렇게 생겨난 게 React라고 할 수 있다.
 
결국 SPA는 React를 위해, React는 SPA를 위해 존재하고 있다고 봐도 무방하다. React는 SPA라는 춤추는 발레리나를 위한 회전무대와도 같다. 발레리나가 춤추는 동안, React는 계속해서 무대를 바꿔나간다. 무대를 교체하거나, 바꿀 필요는 없다. SPA는 계속해서 춤을 추고, React는 그를 위해서 작동한다.
 

 
 
 

6. Props와 State

앞서 나는 state에 대해 "굳이 이렇게 번거로워야 할까?"라는 의문을 던졌다. 사실 단순히 화면에 글자 하나를 바꾸는 일이라면, 기존 JS처럼 document.getElementById().innerText = '변경'이라고 한 줄 쓰면 그만이다. 하지만 React는 우리에게 State라는 생소한 개념을 강요하고 있다. 더군다나 Props라는 새로운 개념도 등장하는데, 이건 부모와 자식 간에 통행증 같은 것이다. 부모? 자식? 그렇다, 컴포넌트 간에는 부모와 자식이 존재한다. 마치 DOM 트리가 펼쳐지는 것처럼, 컴포넌트도 자신만의 나무를 가진다.
 

또, 또, 또! 끝나지 않는 계보도

 
대체 왜 이런 새로운 개념들이 나타나는 걸까? 결론부터 말하자면, 이들은 컴포넌트라는 레고 블록들이 서로 엉키지 않고 질서 있게 대화하기 위한 유일한 방법이기 때문이다. 먼저 Props부터 보자.
 

Props

여기에서 Props는 Properties의 약자이다. 이 Props는 부모 컴포넌트가 자식 컴포넌트에게 전달하는 하나의 데이터다. 하나의 유전자라고도 할 수 있겠다. 우리가 아버지에게 받는 유전자를 생각해보자. 아버지에게서 키, 얼굴, 어쩌면 성격이나 두뇌까지 우리는 유전자로서 전달 받는다. 그런 유전자 데이터들을 React에서는 Props라고 할 수 있다.
 

콧대 props와 눈매 props가 무사히 자식 컴포넌트에 전달됐다.

 
여기서 핵심은 단방향 데이터 흐름이라는 점이다. 자식이 부모에게 유전자를 물려줄 수 없듯, Props또한 부모에게서 자식에게 전달해주는 단방향 데이터다. 자식 입장에서는 읽을 수만 있고, 쓸 수는 없다. 이런 제약은 굳이 React가 아니어도 많은 언어에서 볼 수 있는데, 이유는 간단하다. 자식 컴포넌트가 부모의 데이터를 수정할 수 있게 된다면 의도치 않은 버그가 생길 수 있다. 부모와 자식이 계속 한 집에 살면 매일 갈등이 일어난다는 말처럼, Props의 생태계도 별로 다르지는 않다.


State

컴포넌트 스스로가 변해야 할 때는 어떻게 할까? 클릭하면 숫자가 올라가는 버튼이나, 사용자가 입력하는 검색창처럼 말이다. 이때 등장하는 것이 바로 State라는 개념이다.
Props가 외부에서 주어진 유전자라면(불변하다면), State는 컴포넌트 내부의 상태다. 예전 포스팅에 상태 선택자를 이야기하며 병에 걸렸다고 이야기하는데, 그와 비슷하다. 상태는 영구적인 게 아니라 일시적인 것이다. 다만 위에서 말했듯이, 이 과정이 꽤 번거롭다. useState라는 번거로운 훅을 사용하여, 상태를 하나하나 수정해줘야한다.
 
이 번거로운 과정을 거쳐야 하는 이유는 React의 렌더링 메커니즘 때문이다. React는 드라마에서 흔히 묘사되는 무심한 남자친구 같은 것이다. 헤어스타일을 바꾸든, 화장을 바꾸든, 눈앞에 있는 스마트폰이 더 중요한 것이다. 그러면 우리(여자친구)는 이렇게 물어봐야 한다. "나 어디 변한 거 같지 않아?" 그럼 이제 남자친구의 얼굴이 새하얗게 변하며 어디가 변했는지 확인할 것이다. 이것이 2번에서 다뤘던 렌더링 트리거의 정체다.
 
왜 이렇게 해야할까? React가 추구하는 것이 재사용이 가능하고, 독립해야하는 UI라는 지점에 주목해야한다. 메타는 그동안 수많은 가짜 알림들에 시달려왔고, 따라서 명확하게 관리될 수 있는 것을 원했다. 여기서 Props와 State는 이를 받쳐주는 역할을 한다. Props는 단방향 통신을 통해 흐름을 명확하게 하고, State는 상태가 바뀌었다는 것을 알림으로써 어떤 것이 변화해서 이러한 동작이 벌어졌는지 알 수 있도록 해준다. 이 두 개념이 맞물리면서 React의 컴포넌트는 단순한 HTML 조각을 넘어, 데이터에 반응하여 살아 움직이는 유기체가 된다. 번거로움이라는 비용을 지불하고, 예측 가능성과 관리의 편의성이라는 거대한 이득을 얻는 셈이다.
 
 

7. Hook과 함수 컴포넌트

리액트의 언어 체계를 배우다 보면 반드시 마주치는 거대한 갈림길이 있다. 바로 클래스형 컴포넌트와 함수 컴포넌트다. 과거의 리액트는 클래스형이 주류였지만, 현재는 거의 모든 프로젝트가 함수 컴포넌트와 Hook(훅)을 사용한다.
 

클래스 컴포넌트의 한계

초창기 리액트에서 State를 관리하거나 컴포넌트의 생명주기를 다루려면 반드시 클래스 문법을 써야 했다. 참고로 여기서 생명주기란 컴포넌트 하나의 생성-사용-소멸 과정을 이야기한다. 그야 말로 생명의 라이프사이클(Life-Cycle)이라 할 수 있다. 
여하튼 다시 본문으로 돌아가면, 클래스는 프론트엔드 환경과는 잘 맞지 않는 면이 있었다. JavaScript에도 class 문법이 도입되었지만, 본래 클래스라는 개념은 컴파일 기반 언어를 전제로 설계된 모델에 가깝다. 그래서 리액트에서 클래스를 사용할 때는 this 바인딩, 수명주기 메서드 분산 같은 구조적인 복잡함을 감수해야 했다. 결국 React팀 본인들이 인정하듯, 클래스 기반 컴포넌트는 태생적으로 무겁고 다루기 까다로운 편이었다.
무엇보다 큰 문제는 로직을 재활용하기가 어렵다는 점이다. 비슷한 기능을 가진 컴포넌트를 만들고 싶어도, 클래스 구조 안에서는 필요한 부분만 깨끗하게 떼어내어 다른 곳에 붙이기가 쉽지 않았다. 4번에서 말한 레고 블록 같은 재사용성을 추구하면서도, 정작 그 블록을 찍어내는 틀(클래스)은 지나치게 비대했던 셈이다.
 

Hook의 개발

그러하여 2019년, React는 16.8 버전부터 Hooks라는 구원투수를 등판시킨다. Hook은 말 그대로 함수 컴포넌트에 필요한 기능을 갈고리(Hook)처럼 걸어서 사용할 수 있게 해주는 도구들이다. 대표적으로 사용되는 Hook은 useState와 useEffect이다. useState를 예제로 한번 살펴보자.
 

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      클릭: {count}
    </button>
  );
}

 
이것이 useState를 사용했을 경우 나타나는 Hook이다. 보다시피 버튼을 클릭하면 SetCount를 통하여 count가 1 늘어나는 형태가 된다. 이것을 클래스 컴포넌트로 표현하면 다음과 같다.
 

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        클릭: {this.state.count}
      </button>
    );
  }
}

 
그렇다면 함수형 컴포넌트에서는 어떻게 표현했을까? 그렇다, 사실상 불가능했다. Hook을 사용한 코드를 보자. 함수형 컴포넌트가 압도적으로 편리하다. 함수는 JS에서 가장 다루기 쉽고 가벼운 단위였다. 그러나 단지 state(상태)를 직접 가질 수 있는 방법이 없다는 이유만으로 이를 사용하려면 클래스 컴포넌트를 사용해야만 했다. 
 
 

클래스 컴포넌트와 Hooks의 비교

 
그러하여 Hook의 발명은 함수 컴포넌트에게는 구원이었다. 이제는 그저 아주 단순한 함수 컴포넌트를 작성하고, 필요한 기능이 있을 때마다 적절한 Hook을 골라 쓰면 되게 되었다.
로직을 함수 단위로 쪼갤 수 있게 되면서, 우리는 상태를 관리하는 로직만 따로 떼어내어 커스텀 훅Custom Hook이라는 이름으로 공유할 수 있게 되었다. A라는 컴포넌트에서 썼던 로그인 체크 로직을 B에서도, C에서도 코드 한 줄로 불러와 쓸 수 있게 된 것이다.
React팀은 Hook을 발표하면서 이렇게 말했다.
 

Hook은 우리가 5년간 React로 컴포넌트를 작성하고
유지하는 동안 부딪혔던 수 많은 문제들을 해결했습니다.

 
 
결국 Hook은 리액트의 철학인, 독립성과 재사용성을 문법적으로 완성한 마침표와도 같다. React는 비로소 완전해졌다.
 
 

8. Virtual DOM

이쯤이면 한 가지 의문이 들 것이다.
 

아무리 그렇게 해도 양이 많으면
멀티 페이지가 차라리 낫지 않을까?
양이 많아지다보면 분명히 작동에서 느려질 수밖에 없는데…

 
 
그렇다. 많은 최적화에서 흔히 걱정하는 것은, 문서를 처음부터 끝까지 읽는 로직이 생길 경우다. 우리가 버튼 하나에 색상을 주는 동작을 하더라도, 문서는 처음부터 그 버튼 하나만을 보고 달려가지 못한다. 모든 버튼을 살펴보고 나서야 우리가 원하는 버튼에게 달려갈 수 있다.
그런데 한 페이지에서 정말 많은 컴포넌트가 있을 때, 브라우저는 그 모든 페이지를 읽을 수 있을까? 예를 들어, useState가 여러 개 동시에 발생한다치면 문서 전체에서 이를 읽고 바뀐 state에 따라서 다 로직을 불러야 할 것이다. 그건 정말 끔찍한 일이다. 그래서 React는 바로 Virtual DOM, 즉 가상 DOM을 내놓았다.
 

가상 DOM

사실 가상 DOM이라는 개념은 React가 세상에 나오기 전까지는 듣도 보도 못한 방식이었다. 당시 웹 세상을 지배하던 다른 프레임워크들은 데이터가 바뀌면 화면 전체를 이 잡듯 뒤져서 바뀐 부분을 찾아내는 방식을 사용했다. 이를 일명 더티 체킹이라고 한다. JS에서는 이런 방법이 아주 당연하게 여겨져왔다. 그러나 DOM은 굉장히 무겁고 예민한 녀석이다. 과거 포스팅에서 JS에 대해서 다루며 인터프리터 언어라는 점을 강조했는데, 아무리 컴파일링 방법이 동반된다고 하여도 결국 근본적으로 인터프리터 언어이다. 하나의 DOM을 하나하나 읽어내고 이를 바이트코드로 바꾸는 작업은, 섬세하면서 동시에 "무겁다"라고도 할 수 있다.
그러하여 이러한 무거운 진짜 DOM에서 눈을 돌려 가상 DOM을 메타는 만들기로 했다. 가상 DOM은 일종의 복사본이다. 문구 중에는 트레싱지라고 하는 기름종이가 있는데, 아래에 그림이나 글씨를 대고 따라 그릴 수가 있다. 가상 DOM은 이렇게 따라 그린 것이다.
 
동작 원리는 이렇다. State가 변해서 렌더링 트리거가 당겨지면, React는 곧바로 실제 화면을 고치지 않는다. 대신 메모리 속에 있는 가상 DOM을 먼저 업데이트한다. 그리고 이전 버전의 가상 DOM과 방금 바뀐 가상 DOM을 순식간에 비교한다. 트레싱지 두 장을 겹쳐서 빛에 비춰보면, 다른 부분을 바로 찾아낼 수 있다. 그렇게 순식간에 바뀐 것을 확인하고, 바뀐 부분만을 바로 원본(진짜 DOM)에 업데이트 한다.
 
이전에 비동기 통신의 이점에 대해서 기다릴 필요가 없다고 하는 것과도 비슷하다. 모든 건 물밑에서 이루어지고, 물 밑에서 이루어진 것들을 우리는 그저 빠르게 작동한다고 믿고 있을 뿐이다. 마치 마술처럼.
 
 

9. 마무리하며

이렇게 이번 포스트에서는 React의 언어에 대해서 정리해보는 시간을 가졌다. 아마 특별편에서 React 실습을 하고, 5주차는 본격적으로 java를 다룰 예정이다.