본문 바로가기

React의 핵심 개념과 장점

선언적 UI, 재사용 가능한 컴포넌트, 가상 DOM, 단방향 데이터 흐름이 React 개발을 단순화하는 원리를 익힙니다.

React를 처음 접하면 컴포넌트를 잘게 나누면 되는 도구 정도로 이해하고 넘어가기 쉽습니다. 하지만 컴포넌트보다 먼저 React가 문제를 바라보는 방식을 이해해야 시행착오를 줄일 수 있습니다. 같은 UI를 만들어도 어떤 팀은 빠르게 기능을 늘리고 어떤 팀은 고칠 때마다 예기치 않은 버그를 만나는데, 그 차이도 여기서 생깁니다. 처음에는 문법과 API를 외우는 데 집중하기 쉽지만, 오래 남는 것은 왜 이 구조를 택했는지에 대한 판단 기준입니다.

이 글에서는 React의 네 가지 핵심 개념인 선언적 UI, 컴포넌트 기반 구조, 가상 DOM, 단방향 데이터 흐름을 차례로 살펴봅니다. 각 개념이 무엇인지뿐 아니라 언제 이점이 되고 언제 주의해야 하는지도 함께 다루며, 이 관점을 잡아 두면 뒤의 상태 관리와 성능 최적화, 구조 설계를 훨씬 수월하게 따라갈 수 있습니다.


UI 개발을 위한 선언적 방식

React의 가장 큰 특징 가운데 하나는 선언적(declarative) 방식으로 UI를 만든다는 점입니다. 이는 명령형(imperative) 방식과 대비되는 개념이므로 두 방식을 먼저 비교해 봅니다.

명령형 방식은 어떻게 할 것인가에 초점을 둡니다. 개발자가 단계별로 DOM을 직접 조작해 UI의 변화를 지시하는 방식으로, “이 버튼을 찾아 클릭 이벤트를 붙이고, 클릭하면 글자를 ‘클릭됨’으로 바꾼 뒤 글자색을 빨간색으로 바꿔라”처럼 자세히 명령합니다. 코드가 복잡해질수록 상태를 관리하고 버그를 추적하기 어려워지는 경향이 있습니다.

선언적 방식은 무엇을 보여 줄 것인가에 초점을 둡니다. 개발자는 원하는 UI의 최종 모습을 선언하고, React가 렌더 결과를 비교해 필요한 DOM 변경을 반영합니다. 예를 들어 “버튼의 isActive가 true이면 ‘활성’을 파란색으로, false이면 ‘비활성’을 회색으로 보여 줘”라고 선언해 두면, isActive가 바뀔 때마다 React가 그 상태에 맞는 UI를 그립니다.

이때 React가 변경을 알아차리는 계기는 state의 setter로 갱신을 요청하는 것입니다. setter가 호출되면 React는 새 입력으로 컴포넌트를 호출해 UI를 계산하고, 이전 결과와 달라진 DOM 부분을 반영합니다. 일반 변수나 객체의 속성에 값을 대입하는 것만으로는 React가 그 변경을 감지해 다시 렌더링하지 않습니다(React 문서의 컴포넌트의 기억, state).

이런 선언적 방식은 관련된 DOM을 일일이 찾아 고치는 부담을 덜어 주고, 코드를 읽기 쉽게 하며 UI의 갱신을 예측할 수 있게 만듭니다. 대신 개발자는 상태를 누가 가질지, 언제 갱신할지, 이벤트와 외부 시스템을 어떻게 연결할지를 설계합니다. 초점 이동처럼 DOM을 직접 다뤄야 하는 작업은 따로 떼어 둡니다.

물론 선언적 방식이 언제나 정답은 아닙니다. 캔버스나 WebGL, 복잡한 애니메이션처럼 그리기를 세밀하게 제어해야 하는 영역은 명령형 접근이 더 직접적일 때가 있습니다. 그래서 모든 것을 선언형으로 밀어붙이기보다, UI 상태의 동기화가 중요한 영역에 선언형을 먼저 적용하고 저수준 제어가 필요한 부분만 분리하는 편이 더 안정적입니다.


컴포넌트 기반 아키텍처

React의 또 다른 특징은 UI를 컴포넌트(component)라는 조각으로 나눠 개발한다는 점입니다. 컴포넌트는 UI를 이루는 독립적이고 재사용할 수 있는 단위이며, 작은 컴포넌트를 조합해 복잡한 UI를 만듭니다. 웹페이지라면 헤더, 내비게이션 바, 사이드바, 게시물 목록, 댓글 입력창을 각각 하나의 컴포넌트로 만들 수 있습니다.

컴포넌트로 나누면 몇 가지 이점이 생깁니다. 첫째는 재사용성(reusability)으로, 한 번 만든 컴포넌트를 다른 페이지나 프로젝트에서 다시 쓸 수 있습니다. 버튼 컴포넌트를 만들어 두면 사이트의 모든 버튼에 같은 스타일과 기능을 적용하면서도, 필요에 따라 여러 종류의 버튼을 쉽게 만들어 낼 수 있습니다.

둘째는 유지보수성(maintainability)입니다. 입력과 역할이 분명한 컴포넌트는 바뀌는 이유와 확인할 범위를 좁혀 줍니다. 다만 부모의 props나 공유 상태가 잘못되면 여러 자식이 함께 영향을 받으므로, 컴포넌트를 나누는 것만으로 오류가 격리되지는 않습니다. 셋째는 모듈성(modularity)으로, 복잡한 UI를 작은 단위로 나눠 개발하므로 여러 개발자가 서로 다른 컴포넌트를 동시에 맡아 협업할 수 있습니다.

이런 컴포넌트 기반 접근은 큰 애플리케이션에 특히 유리하며, 관련된 코드를 한곳에 모으고 서로의 의존을 줄여 소프트웨어의 전반적인 품질을 높입니다. 컴포넌트를 나눌 때는 화면 조각만 보지 말고, 상태를 누가 가지고 어떤 값이 props로 내려가는지도 함께 정해야 합니다. 컴포넌트를 나누는 선은 UI의 역할과 state를 가진 쪽이 함께 드러나도록 긋습니다.


가상 DOM

데이터가 바뀌면 UI를 효율적으로 갱신한다는 React의 선언적 방식을 떠받치는 기술 가운데 하나가 가상 DOM(Virtual DOM)입니다. 웹 브라우저는 HTML 문서를 파싱해 화면에 보여 주기 위한 트리 형태의 객체 모델을 만드는데, 이것을 DOM(Document Object Model)이라고 부릅니다.

DOM 작업의 비용은 무엇을 바꾸는지와 읽고 쓰는 순서에 따라 달라집니다. 글자 하나를 바꾸는 작업과 많은 요소의 크기를 반복해서 읽고 바꾸는 작업을 같은 비용으로 볼 수는 없습니다. DOM의 변경은 그 종류와 브라우저의 처리에 따라 스타일 계산과 레이아웃, 페인트를 일으킬 수 있으며, 변경할 때마다 이 과정이 모두 즉시 실행되는 것은 아닙니다. 그래도 DOM을 자주 조작하면 웹 애플리케이션의 성능이 떨어질 수 있습니다.

React는 이 문제를 줄이려고 가상 DOM이라는 개념을 도입했습니다. 가상 DOM은 다음에 보여 줄 UI를 기술하는 자바스크립트 표현으로 이해할 수 있으며, 실제 DOM 노드와 이벤트, 브라우저의 상태를 완전히 복제한 사본은 아닙니다. React는 실제 DOM을 바로 조작하지 않고 다음 과정을 거칩니다.

React 렌더 데이터 흐름 — 상태 추적

본문 카운터 1→2에서 현재 렌더 입력·다음 UI 계산·필요 DOM commit이 다르다. 같은 위치의 입력과 텍스트를 구분해 DOM 전부 재생성 오해를 줄일 가치가 있다.

카운터1→2이면 제목 텍스트가1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.

React · UI와 실행의 관계

Render는 UI를 계산하고 commit은 DOM에 반영합니다

상태의 setter는 렌더를 요청합니다. 컴포넌트 계산과 DOM 반영을 구분하고, 결과가 그대로인 DOM은 바꾸지 않는 경계를 읽습니다.

state와 props에서 render·commit까지현재state·props→컴포넌트render→이전과다음결과비교→필요한DOMcommit의4노드3간선이다. render중기존객체/DOM을바꾸지않으며 결과가같으면해당DOM을변경하지않는다. commit→state자동변경의순환간선은없다.
state · props현재 렌더의 입력
연결 관계state · props · 현재 렌더의 입력 → Render · 컴포넌트 호출 · 다음 UI 계산
Render컴포넌트 호출다음 UI 계산
연결 관계Render · 컴포넌트 호출 · 다음 UI 계산 → 결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산
결과 비교이전 · 다음 UI필요한 DOM 변경 계산
연결 관계결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산 → DOM commit · 차이가 있는 부분 반영 · 같은 결과는 유지
DOM commit차이가 있는 부분 반영같은 결과는 유지
state와 props에서 render·commit까지 · 모바일현재state·props→컴포넌트render→이전과다음결과비교→필요한DOMcommit의4노드3간선이다. render중기존객체/DOM을바꾸지않으며 결과가같으면해당DOM을변경하지않는다. commit→state자동변경의순환간선은없다. 모바일에서도 같은 연결과 분기·되돌림 대상을 유지한다.
state · props현재 렌더의 입력
연결 관계state · props · 현재 렌더의 입력 → Render · 컴포넌트 호출 · 다음 UI 계산
Render컴포넌트 호출다음 UI 계산
연결 관계Render · 컴포넌트 호출 · 다음 UI 계산 → 결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산
결과 비교이전 · 다음 UI필요한 DOM 변경 계산
연결 관계결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산 → DOM commit · 차이가 있는 부분 반영 · 같은 결과는 유지
DOM commit차이가 있는 부분 반영같은 결과는 유지

본문 장바구니의 qty는 부모에서 자식 입력으로 내려갑니다. 자식의 이벤트는 전달받은 함수를 호출해 부모에 갱신을 요청하며, 새 입력은 다음 렌더에서 다시 내려옵니다.

카운터1→2이면 제목 텍스트가1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 캡션카운터 1→2이면 제목 텍스트가 1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.
전체 관계·조건 설명현재 state·props→컴포넌트 render→이전과 다음 결과 비교→필요한 DOM commit의 노드 4개·간선 3개다. render 중 기존 객체/DOM을 바꾸지 않으며 결과가 같으면 해당 DOM을 변경하지 않는다. commit→state 자동 변경의 순환 간선은 없다.
전체 관계·조건 설명현재 state·props→컴포넌트 render→이전과 다음 결과 비교→필요한 DOM commit의 노드 4개·간선 3개다. render 중 기존 객체/DOM을 바꾸지 않으며 결과가 같으면 해당 DOM을 변경하지 않는다. commit→state 자동 변경의 순환 간선은 없다. 모바일에서도 같은 연결과 분기·되돌림 대상을 유지한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 commitstate: 1 / DOM: count 1·기존 input카운터 1이 표시되고 정해진 위치의 입력 DOM이 있다입력 DOM의 브라우저 상태와 count 텍스트를 구분한다
render2render input: 2 / DOM: 아직 1state 2로 다음 UI를 계산하되 DOM은 아직 이전 commit이다컴포넌트 호출이 DOM 전체 교체를 뜻하지는 않는다
commit2텍스트: 2 / input: 같은 위치/종류 유지 조건달라진 count 텍스트는 2가 되고 필요한 입력 정체성은 유지된다element 설명은 실제 DOM 상태 전체의 복사본이 아니다

React 렌더 데이터 흐름 — 상태 추적

본문 카운터 1→2에서 현재 렌더 입력·다음 UI 계산·필요 DOM commit이 다르다. 같은 위치의 입력과 텍스트를 구분해 DOM 전부 재생성 오해를 줄일 가치가 있다.

카운터1→2이면 제목 텍스트가1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.

React · UI와 실행의 관계

Render는 UI를 계산하고 commit은 DOM에 반영합니다

상태의 setter는 렌더를 요청합니다. 컴포넌트 계산과 DOM 반영을 구분하고, 결과가 그대로인 DOM은 바꾸지 않는 경계를 읽습니다.

state와 props에서 render·commit까지현재state·props→컴포넌트render→이전과다음결과비교→필요한DOMcommit의4노드3간선이다. render중기존객체/DOM을바꾸지않으며 결과가같으면해당DOM을변경하지않는다. commit→state자동변경의순환간선은없다.
state · props현재 렌더의 입력
연결 관계state · props · 현재 렌더의 입력 → Render · 컴포넌트 호출 · 다음 UI 계산
Render컴포넌트 호출다음 UI 계산
연결 관계Render · 컴포넌트 호출 · 다음 UI 계산 → 결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산
결과 비교이전 · 다음 UI필요한 DOM 변경 계산
연결 관계결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산 → DOM commit · 차이가 있는 부분 반영 · 같은 결과는 유지
DOM commit차이가 있는 부분 반영같은 결과는 유지
state와 props에서 render·commit까지 · 모바일현재state·props→컴포넌트render→이전과다음결과비교→필요한DOMcommit의4노드3간선이다. render중기존객체/DOM을바꾸지않으며 결과가같으면해당DOM을변경하지않는다. commit→state자동변경의순환간선은없다. 모바일에서도 같은 연결과 분기·되돌림 대상을 유지한다.
state · props현재 렌더의 입력
연결 관계state · props · 현재 렌더의 입력 → Render · 컴포넌트 호출 · 다음 UI 계산
Render컴포넌트 호출다음 UI 계산
연결 관계Render · 컴포넌트 호출 · 다음 UI 계산 → 결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산
결과 비교이전 · 다음 UI필요한 DOM 변경 계산
연결 관계결과 비교 · 이전 · 다음 UI · 필요한 DOM 변경 계산 → DOM commit · 차이가 있는 부분 반영 · 같은 결과는 유지
DOM commit차이가 있는 부분 반영같은 결과는 유지

본문 장바구니의 qty는 부모에서 자식 입력으로 내려갑니다. 자식의 이벤트는 전달받은 함수를 호출해 부모에 갱신을 요청하며, 새 입력은 다음 렌더에서 다시 내려옵니다.

카운터1→2이면 제목 텍스트가1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 캡션카운터 1→2이면 제목 텍스트가 1→2로 바뀔 수 있지만, 같은 위치의 독립된 입력을 매번 새로 만들지는 않습니다. 가상 DOM은 실제 DOM·브라우저 상태의 완전한 복사본이 아닙니다. 렌더 계산도 비용을 가지므로 모든 구현보다 빠르다는 뜻은 아닙니다.
전체 관계·조건 설명현재 state·props→컴포넌트 render→이전과 다음 결과 비교→필요한 DOM commit의 노드 4개·간선 3개다. render 중 기존 객체/DOM을 바꾸지 않으며 결과가 같으면 해당 DOM을 변경하지 않는다. commit→state 자동 변경의 순환 간선은 없다.
전체 관계·조건 설명현재 state·props→컴포넌트 render→이전과 다음 결과 비교→필요한 DOM commit의 노드 4개·간선 3개다. render 중 기존 객체/DOM을 바꾸지 않으며 결과가 같으면 해당 DOM을 변경하지 않는다. commit→state 자동 변경의 순환 간선은 없다. 모바일에서도 같은 연결과 분기·되돌림 대상을 유지한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 commitstate: 1 / DOM: count 1·기존 input카운터 1이 표시되고 정해진 위치의 입력 DOM이 있다입력 DOM의 브라우저 상태와 count 텍스트를 구분한다
render2render input: 2 / DOM: 아직 1state 2로 다음 UI를 계산하되 DOM은 아직 이전 commit이다컴포넌트 호출이 DOM 전체 교체를 뜻하지는 않는다
commit2텍스트: 2 / input: 같은 위치/종류 유지 조건달라진 count 텍스트는 2가 되고 필요한 입력 정체성은 유지된다element 설명은 실제 DOM 상태 전체의 복사본이 아니다

위 그림은 현재 렌더의 입력인 state와 props에서 시작해 DOM에 반영하기까지의 네 단계를 보여 줍니다. 먼저 render 단계에서 React가 컴포넌트를 호출해 다음 UI를 계산합니다. 이어 이전 UI와 다음 UI를 비교해 필요한 DOM 변경을 계산하고, 마지막 commit 단계에서 차이가 있는 부분만 실제 DOM에 반영하며 결과가 같은 부분은 그대로 둡니다. 이 비교에 쓰는 효율적인 알고리즘을 흔히 diffing 알고리즘이라고 부릅니다(React의 render와 commit).

그림의 설명처럼 render하는 동안에는 기존 객체나 DOM을 바꾸지 않고, commit이 끝났다고 state가 저절로 다시 바뀌는 순환도 없습니다. 그림 위쪽의 예처럼 카운터가 1에서 2로 바뀌면 제목의 글자는 1에서 2로 바뀔 수 있지만, 같은 위치에 있는 독립된 입력 요소를 매번 새로 만들지는 않습니다. 또 렌더 계산에도 비용이 들므로, 가상 DOM이 있다는 사실이 모든 구현보다 빠르다는 뜻은 아닙니다.

이 과정 덕분에 개발자는 실제 DOM 조작 명령을 일일이 쓰지 않고 state만 갱신하면 되고, React가 필요한 DOM 변경을 계산해 불필요한 조작을 줄입니다. 다만 실제 성능은 컴포넌트의 계산량과 브라우저의 스타일·레이아웃·페인트 비용에도 달려 있습니다. 가상 DOM을 쓴다는 사실만으로 DOM을 직접 다루는 모든 구현보다 빠르다고 판단하지 말고, 성능은 측정해서 확인합니다.


단방향 데이터 흐름

React는 단방향 데이터 흐름(unidirectional data flow)을 따릅니다. 여기서 말하는 방향은 부모 컴포넌트가 자식에게 props로 입력을 넘기는 방향이며, 자식의 클릭 이벤트나 갱신 요청이 부모에게 전달될 수 없다는 뜻은 아닙니다.

자식 컴포넌트는 부모에게서 받은 props를 읽을 수만 있고, 부모의 데이터를 직접 바꿀 수는 없습니다. props는 읽기 전용 입력으로 다룹니다. 객체를 넘겼다고 해서 자바스크립트가 그 내부 속성까지 자동으로 잠그지는 않지만, 자식이 받은 객체를 직접 고치면 이 약속을 깨므로 그렇게 쓰지 않습니다(React 문서의 props 전달). 자식이 부모의 데이터를 바꿔야 한다면, 부모가 넘겨준 함수를 호출해 부모 안에서 상태를 바꾸도록 요청합니다.

이런 단방향 흐름은 데이터의 변화를 추적하기 쉽게 만듭니다. 데이터가 어느 방향으로든 자유롭게 흐른다면 예측하기 어려운 버그가 생기기 쉽고 원인을 찾기도 어렵지만, 흐름을 한 방향으로 제한하면 애플리케이션이 안정적이고 디버깅도 수월해집니다. 반대로 컴포넌트가 깊어질수록 props를 여러 단계로 전달해야 하는 부담이 생기므로, 어느 정도 규모가 커지면 Context나 전역 상태 관리 도구, 서버 상태 도구를 상황에 맞게 들여 흐름을 단순하게 만듭니다. 이때도 단방향 원칙을 버리는 것이 아니라, 원칙은 지킨 채 전달 경로를 관리하기 쉬운 형태로 바꾸는 것입니다.

아래 코드는 개발 환경 설정과 첫 번째 React 앱 만들기를 마친 뒤 Vite 프로젝트의 src/App.jsx를 바꿔 넣어 실행할 수 있습니다. 지금은 부모인 App이 수량을 기억하고 자식인 CartLine이 그 값을 읽는 순서를 따라가 보세요. useState는 두 값이 든 배열을 돌려주고, const [qty, setQty]는 그 배열의 요소를 순서대로 꺼내 변수에 담는 배열 구조 분해(array destructuring)입니다. qty는 현재 수량, setQty는 다음 수량을 요청하는 함수입니다.

src/App.jsx · 하나의 수량에서 목록과 합계를 계산하기
import { useState } from 'react';

function CartLine({ qty, onAdd }) {
  return (
    <section>
      <p>상품 수량: {qty}</p>
      <button type="button" onClick={onAdd}>하나 추가</button>
    </section>
  );
}

export default function App() {
  const [qty, setQty] = useState(0);
  const total = 1200 * qty;

  return (
    <>
      <p>장바구니 배지: {qty}</p>
      <CartLine qty={qty} onAdd={() => setQty((previous) => previous + 1)} />
      <p>합계: {total}원</p>
      <button type="button" disabled={qty === 0}>결제</button>
    </>
  );
}

처음에는 수량과 배지, 합계가 모두 0이고 결제 버튼은 비활성입니다. 하나 추가 버튼을 한 번 누르면 자식이 onAdd를 호출하고, 그 함수가 부모의 setter를 실행합니다. 다음 렌더에서 qty는 1, total은 1200이 되어 화면에는 상품 수량: 1, 장바구니 배지: 1, 합계: 1200원이 보이고, 세 표시와 결제 버튼이 같은 입력을 씁니다.

total은 따로 둔 state가 아니라 렌더할 때마다 qty에서 계산한 값입니다. 또 onClick={onAdd}는 함수 자체를 넘기는 것이라서, 화면을 계산하는 동안에는 호출되지 않고 클릭했을 때 호출됩니다. 그림 아래쪽의 설명처럼 qty는 부모에서 자식의 입력으로 내려가고, 자식의 이벤트는 넘겨받은 함수를 호출해 부모에게 갱신을 요청하며, 새 입력은 다음 렌더에서 다시 내려옵니다.


React의 장점 요약

지금까지 살펴본 핵심 개념을 바탕으로 React의 주요 장점을 다시 정리해 봅니다. 먼저 생산성이 높아집니다. 선언적 UI와 컴포넌트의 재사용, 필요한 부분만 갱신하는 구조 덕분에 UI 구현에 드는 노력이 줄어, 개발자가 핵심 비즈니스 로직에 집중할 수 있습니다. 성능을 관리할 구조도 생깁니다. UI 계산과 DOM 반영을 구분하므로 불필요한 작업을 살펴볼 수 있으며, 실제 응답 속도는 컴포넌트의 계산량과 브라우저 처리를 측정해 판단합니다.

유지보수도 쉬워집니다. 컴포넌트로 나눈 구조와 단방향 데이터 흐름은 코드를 읽기 쉽게 하고 복잡성을 줄여, 오랫동안 고쳐 나가기 좋게 만듭니다. 생태계와 커뮤니티도 활발합니다. 많은 기여자와 유지 관리자가 React를 발전시키고 있고, 전 세계의 개발자가 쓰는 만큼 자료와 라이브러리가 풍부해 문제가 생겼을 때 도움을 얻기 쉽습니다. 마지막으로 React의 핵심 개념은 웹뿐 아니라 React Native로 만드는 모바일 앱이나 Electron과 함께 만드는 데스크톱 앱에도 이어지므로, 한 번 배운 내용을 넓게 쓸 수 있습니다.

정리하면 React의 핵심 개념은 모두 상태의 변화가 어떤 화면 결과로 이어지는지를 더 읽기 쉽게 만들기 위한 장치입니다. 상태가 자주 바뀌고, 재사용할 부분이 많고, 변경을 추적해야 하는 화면일수록 React의 구조적 장점이 분명해집니다.


정리

  • 선언적 방식에서는 원하는 UI를 선언하고, setter로 state의 갱신을 요청하면 React가 필요한 DOM 변경을 반영합니다. 일반 변수에 값을 대입하는 것만으로는 다시 렌더링되지 않습니다.
  • 컴포넌트는 재사용성과 유지보수성, 모듈성을 높이지만, 나누는 것만으로 오류가 격리되지는 않으므로 state를 가진 쪽과 props의 흐름을 함께 정합니다.
  • 가상 DOM은 다음 UI를 기술하는 자바스크립트 표현이며, render에서 계산하고 비교한 뒤 commit에서 달라진 부분만 DOM에 반영합니다.
  • 가상 DOM이 있다고 항상 더 빠른 것은 아니며, 실제 성능은 계산량과 브라우저 처리를 측정해 판단합니다.
  • props는 부모에서 자식으로 내려가는 읽기 전용 입력이고, 자식은 부모가 넘긴 함수를 호출해 갱신을 요청합니다.
  • 합계처럼 다른 state에서 계산할 수 있는 값은 따로 저장하지 않고 렌더할 때 계산합니다.

연습 문제

1. 가격과 시작 수량 바꾸기

위 코드에서 가격 상수 1200만 1500으로, 시작 수량 useState(0)만 useState(1)로 바꾼 뒤 새로 실행해 하나 추가 버튼을 두 번 누릅니다. 수량과 배지, 합계, 결제 버튼의 상태는 어떻게 될까요?

해설 보기

시작 수량이 1이므로 두 번 누르면 수량과 배지가 3이 되고, 합계는 1500 × 3 = 4500이라 화면에 합계: 4500원이 보입니다. 수량이 0이 아니므로 결제 버튼은 활성 상태입니다.

2. setter를 부르지 않는 onAdd

원래 코드로 돌아와 App이 넘기는 onAdd만 () => qty + 1로 바꾸고 버튼을 한 번 누르면 무엇이 달라질까요?

해설 보기

이 함수는 1을 계산해 돌려줄 뿐 setter를 호출하지 않으므로, React는 다시 렌더링하지 않습니다. 수량과 배지, 합계는 0으로 남고 결제 버튼도 비활성 상태 그대로입니다.

값을 계산하는 것과 React에 상태의 갱신을 요청하는 것은 다른 일입니다. 클릭이라는 요청은 자식에서 부모의 함수로 올라가지만, 다음 qty 입력은 다시 부모에서 자식으로 내려간다는 점도 함께 기억해 둡니다.

다음 글 개발 환경 설정에서는 Node.js와 npm을 설치하고, 코드 편집기와 브라우저의 React 개발자 도구를 준비해 React 앱을 만들 환경을 갖춥니다.