React는 컴포넌트 기반 UI, 선언적 렌더링, 상태 중심 뷰를 대중화해 페이지 중심 코드에서 재사용 가능한 시스템과 패턴으로 팀들의 작업 방식을 바꿨습니다.

React는 단순히 새로운 라이브러리를 소개한 것이 아니라, 팀들이 “프론트엔드 아키텍처”라 부르는 것의 의미를 바꿨습니다. 실무적으로 프론트엔드 아키텍처는 UI 코드베이스를 대규모로 이해가능하게 유지하는 일련의 결정입니다: UI를 어떻게 나누는지, 데이터가 어떻게 흐르는지, 상태는 어디에 저장되는지, 사이드 이펙트(데이터 패칭 등)를 어떻게 처리하는지, 그리고 결과를 어떻게 테스트 가능하고 팀 전체에 일관되게 유지하는지 같은 것들입니다.
컴포넌트 사고는 UI의 각 조각을 렌더링을 소유하고 다른 단위들과 조합해 전체 페이지를 구성할 수 있는 작고 재사용 가능한 단위로 다루는 것입니다.
React가 널리 퍼지기 전에는 많은 프로젝트가 페이지와 DOM 조작 중심으로 조직되어 있었습니다: “이 요소를 찾아 텍스트를 바꿔라, 이 클래스를 토글해라” 같은 식이었죠. React는 팀들을 다른 기본값으로 밀어넣었습니다:
이 아이디어들은 일상의 작업을 바꿨습니다. 코드 리뷰는 "이 상태는 어디에 있어야 하나?"를 묻기 시작했고, 디자이너와 엔지니어는 공통의 컴포넌트 어휘로 정렬할 수 있게 되었으며 팀은 전체 페이지를 다시 작성하지 않고도 UI 빌딩 블록 라이브러리를 성장시킬 수 있었습니다.
팀이 나중에 다른 프레임워크로 옮기더라도 React가 만든 습관들은 남습니다: 컴포넌트 기반 아키텍처, 선언적 렌더링, 예측 가능한 데이터 흐름, 그리고 일회성 페이지 코드보다 재사용 가능한 디자인 시스템 컴포넌트를 선호하는 태도 등입니다. React는 이러한 패턴을 자연스럽게 만들었고, 그 영향은 넓은 프론트엔드 생태계에 퍼졌습니다.
React 이전에는 많은 팀이 페이지 중심으로 인터페이스를 구축했습니다. 서버에서 렌더링되는 템플릿(PHP, Rails, Django, JSP 등)이 HTML을 생성하고, 그 위에 jQuery로 상호작용을 추가하는 방식이 일반적이었습니다.
페이지를 렌더링한 뒤 스크립트로 활성화(activate)했습니다: 날짜 선택기, 모달 플러그인, 폼 검증기, 캐러셀 등—각각 자신의 마크업 기대치와 이벤트 훅을 가졌습니다.
코드는 종종 DOM 노드를 찾아 핸들러를 붙이고 DOM을 변경하는 식이었고, 아무것도 망가지지 않기를 바라는 식이었습니다. UI가 커지면 진실의 원천(source of truth)은 조용히 DOM 자체가 되곤 했습니다.
UI 동작은 한 곳에 모여있지 않았습니다. 보통 다음으로 나뉘었습니다:
예를 들어 결제 요약 위젯 하나가 서버에서 부분적으로 만들어지고, AJAX로 업데이트되고, 플러그인으로 일부를 제어하는 식으로 나뉠 수 있었습니다.
이 접근법은 작은 개선에는 잘 작동했지만 반복적인 문제를 낳았습니다:
Backbone, AngularJS, Ember 같은 프레임워크는 모델·뷰·라우팅으로 구조를 가져오려 했습니다—대부분 큰 개선이었지만, 많은 팀은 여전히 패턴을 섞어 쓰며 반복 가능한 단위로 UI를 구성할 더 단순한 방법에 대한 틈을 남겼습니다.
React의 가장 중요한 전환은 간단히 말하면 강력합니다: UI는 상태의 함수다. DOM을 "진실의 원천"으로 취급하고 수동으로 동기화하는 대신, 데이터를 진실의 원천으로 두고 UI를 그 결과로 취급합니다.
상태는 화면이 의존하는 현재 데이터입니다: 메뉴가 열려 있는지, 폼에 입력된 텍스트, 리스트의 항목들, 선택된 필터 등입니다.
상태가 바뀌면 페이지를 뒤져 여러 DOM 노드를 업데이트할 필요가 없습니다. 상태를 업데이트하면 UI가 그에 맞춰 다시 렌더됩니다.
전통적 DOM-우선 코드는 보통 흩어진 업데이트 로직을 만들었습니다:
React 모델에서는 이런 업데이트들이 렌더 출력의 조건이 됩니다. 화면은 주어진 상태에서 무엇이 보여져야 하는지에 대한 읽기 쉬운 설명이 됩니다.
function ShoppingList() {
const [items, setItems] = useState([]);
const [text, setText] = useState("");
const add = () => setItems([...items, text.trim()]).then(() => setText(""));
return (
<section>
<form onSubmit={(e) => { e.preventDefault(); add(); }}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button disabled={!text.trim()}>Add</button>
</form>
{items.length === 0 ? <p>No items yet.</p> : (
<ul>{items.map((x, i) => <li key={i}>{x}</li>)}</ul>
)}
</section>
);
}
빈 메시지, 버튼의 disabled 상태, 리스트 내용이 모두 items와 text에서 파생된다는 점을 주목하세요. 이것이 아키텍처적 이득입니다: 데이터 구조와 UI 구조가 정렬되어 화면을 이해하고 테스트하며 확장하기 쉬워집니다.
React는 “컴포넌트”를 기본 UI 작업 단위로 만들었습니다: 명확한 인터페이스 뒤에 마크업, 동작, 스타일링 훅을 묶은 작고 재사용 가능한 조각입니다.
HTML 템플릿, 이벤트 리스너, CSS 셀렉터를 관련 없는 파일에 흩어놓는 대신, 컴포넌트는 이동하는 부분들을 가깝게 유지합니다. 모든 것을 한 파일에 넣어야 한다는 의미는 아니지만, 코드는 사용자가 보고 상호작용하는 것을 중심으로 조직됩니다.
실무적 관점에서 컴포넌트는 보통 다음을 포함합니다:
중요한 전환은 “이 div를 업데이트하라”는 사고에서 “이 상태에서 버튼을 비활성화된 상태로 렌더하라”는 사고로 바뀐다는 점입니다.
컴포넌트가 적은 수의 props(입력)와 이벤트/콜백(출력)을 노출하면 내부를 바꿔도 앱의 다른 부분을 망가뜨리지 않고 개선할 수 있습니다. 팀은 특정 컴포넌트나 폴더(예: "checkout UI")를 소유하고 자신 있게 개선할 수 있습니다.
캡슐화는 우발적 결합을 줄입니다: 전역 셀렉터가 줄고, 크로스-파일 사이드 이펙트가 줄며, "왜 이 클릭 핸들러가 작동을 멈췄지?" 같은 놀람이 줄어듭니다.
컴포넌트가 주요 빌딩 블록이 되자 코드가 제품을 그대로 반영하기 시작했습니다:
이 매핑 덕분에 디자이너, PM, 엔지니어가 같은 "사물"에 대해 이야기하기 쉬워졌습니다.
컴포넌트 사고는 많은 코드베이스를 기능 또는 도메인 기반 조직(/checkout/components/CheckoutForm 등)과 공유 UI 라이브러리(/ui/Button) 쪽으로 밀었습니다. 기능이 확장될 때 페이지 전용 폴더보다 이 구조가 더 잘 확장되며, 나중에 디자인 시스템을 도입할 기반을 마련합니다.
React의 렌더링 스타일은 흔히 선언적이라고 불립니다. 즉, 주어진 상황에서 UI가 어떻게 보여야 하는지를 선언하면 React가 브라우저를 그 상태로 맞추는 방법을 알아서 처리한다는 뜻입니다.
옛 DOM-우선 접근법에서는 단계별 지시를 작성했습니다:
선언적 렌더링에서는 대신 결과를 표현합니다:
사용자가 로그인되어 있으면 이름을 보여주고, 아니면 "Sign in" 버튼을 보여라.
이 전환은 "UI 장부 관리"의 양을 줄여줍니다. 어떤 요소가 존재하는지, 무엇을 업데이트해야 하는지 계속 추적할 필요가 없고, 앱이 가질 수 있는 상태에 집중할 수 있습니다.
JSX는 UI 구조를 그 구조를 제어하는 로직과 가까이 쓰기 편하게 했습니다. 템플릿 파일과 로직 파일을 분리해 이리저리 오갈 필요 없이 관련 조각을 함께 둘 수 있습니다: 마크업 같은 구조, 조건, 작은 포맷 결정, 이벤트 핸들러를 한곳에 두는 것이죠.
이 근접성(co-location)은 React의 컴포넌트 모델이 실용적으로 느껴진 큰 이유입니다. 컴포넌트는 단순한 HTML 덩어나 JavaScript 번들이 아니라 UI 동작의 단위입니다.
JSX가 HTML과 JavaScript를 섞는다는 우려가 있습니다만, JSX는 실제로 HTML이 아니라 JavaScript 호출을 생성하는 문법입니다. 더 중요한 점은 React가 기술을 섞는 것이 아니라 함께 변하는 것들을 묶는 것입니다. 예: "검증에 실패할 때만 오류 메시지를 보여라" 같은 규칙이 논리와 UI 구조가 밀접하게 연결된 상황에서 한 곳에 있는 것이 여러 파일에 흩어져 있는 것보다 명확할 수 있습니다.
JSX가 React를 접근하기 쉽게 했지만, 근본 개념은 JSX를 넘어 확장됩니다. JSX 없이 React를 쓸 수도 있고, 다른 프레임워크들도 다른 템플릿 문법으로 선언적 렌더링을 사용합니다. 지속적인 영향은 사고방식입니다: UI를 상태의 함수로 취급하고 프레임워크에 화면 동기화의 세부를 맡기세요.
React 이전의 흔한 버그 원인은 단순했습니다: 데이터는 변경됐지만 UI는 변경되지 않았다. 개발자는 새 데이터를 가져온 뒤 적절한 DOM 노드를 찾아 텍스트를 업데이트하고, 클래스를 토글하고, 요소를 추가/제거하며 모든 것을 일관되게 유지하려 애썼습니다. 시간이 지나며 "업데이트 로직"이 실제 UI보다 더 복잡해지는 경우도 많았습니다.
React의 큰 작업 흐름 전환은 브라우저에 페이지를 어떻게 변경할지 지시하지 않는다는 점입니다. 주어진 상태에서 UI가 어떻게 보여야 하는지를 기술하면 React가 실제 DOM을 일치시키기 위한 최소 변경을 계산합니다.
리콘실리에이션은 React가 지난 렌더 출력과 이번 렌더 출력을 비교하고 실제 DOM에 가장 작은 변경 집합만 적용하는 과정입니다.
중요한 부분은 React가 "가상 DOM"을 성능의 마법으로 사용하는 것이 아니라 예측 가능한 모델을 제공한다는 점입니다:
이 예측 가능성은 개발자 워크플로를 개선합니다: 수동 DOM 업데이트가 줄고, 비일관 상태가 줄며, UI 업데이트가 앱 전반에서 동일한 규칙을 따릅니다.
리스트 렌더링 시 React는 "이전 항목"과 "새 항목"을 매칭할 안정적인 방법이 필요합니다. 그게 key입니다.
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
ID 같은 안정적이고 고유한 키를 사용하세요. 항목이 재정렬되거나 삽입/삭제될 수 있다면 배열 인덱스는 피해야 합니다—아니면 React가 잘못된 컴포넌트 인스턴스를 재사용해 입력값이 엉키는 등 놀라운 동작이 발생할 수 있습니다.
React는 프론트엔드 아키텍처를 몇 가지 핵심 결정으로 재구성했습니다:
실제 효과는 수작업 DOM 관리가 줄고 팀과 도구에 대한 경계가 더 명확해진다는 점입니다.
컴포넌트 사고(component thinking)는 각 UI 조각을 렌더링을 소유하고 더 큰 화면으로 조합 가능한 작은 재사용 단위로 취급하는 것입니다. 실무적으로 컴포넌트는 다음을 번들로 가집니다:
이 접근은 “이 DOM 노드를 업데이트하라”는 사고에서 “이 상태에서 이 컴포넌트를 렌더링하라”는 사고로 전환시켜 줍니다.
DOM-우선 코드에서는 DOM이 진실의 원천이 되어 여러 요소를 수동으로 동기화하곤 했습니다. React에서는 상태를 업데이트하면 그 상태를 바탕으로 렌더링이 일어나므로 로딩 스피너, 버튼의 disabled 상태, 빈 상태 메시지 등이 자연스럽게 일관성을 유지합니다.
간단한 기준: "요소를 찾아서 클래스를 토글하는" 동작을 많이 쓰고 있다면 프레임워크 모델과 싸우고 있는 것입니다. UI가 어긋난다면 대개는 상태 소유권 문제입니다.
React 이전의 많은 앱은 페이지 중심 접근이었다: 서버 렌더링 템플릿 + jQuery + 플러그인. 동작 로직은 서버 뷰, HTML 속성, JS 초기화 코드에 흩어져 있었습니다.
흔한 문제는 다음과 같았습니다:
React는 팀을 재사용 가능한 컴포넌트와 예측 가능한 업데이트 패턴 쪽으로 밀어넣었습니다.
선언적 렌더링은 특정 상태에서 UI가 어떻게 보여야 하는지를 ‘설명’하는 것이고, DOM을 단계별로 조작하는 방식이 아닙니다.
전통적인 방법에서는 노드를 만들고 텍스트를 설정하고 붙이고 나중에 제거하는 식이었지만, 선언적 렌더링에서는 렌더 출력에 조건을 써서 “로그인 상태이면 이름을 보여주고 아니면 로그인 버튼을 보여라”처럼 표현합니다. React가 실제 DOM을 업데이트하는 일을 처리합니다.
JSX는 UI 구조를 그와 관련된 로직(조건, 포맷, 핸들러)과 가깝게 함께 작성할 수 있게 해 주었기 때문에 React 모델이 빠르게 자리잡는 데 큰 도움이 됐습니다. JSX는 HTML이 아니라 JavaScript 호출을 생성하는 문법이라는 점을 기억하세요. 핵심 이득은 조직화에 있습니다: 함께 변하는 것들을 한 곳에 묶어두면 유지보수가 더 쉬워집니다.
리콘실리에이션(reconciliation)은 React가 이전 렌더와 이번 렌더를 비교하고 실제 DOM에 최소한의 변경만 적용하는 과정입니다.
핵심은 가상 DOM 자체가 마법 같은 성능 트릭이라기보다, 예측 가능한 모델을 제공한다는 점입니다: 렌더 로직을 마치 UI 전체를 새로 그리는 것처럼 쓰면 React가 증분적으로 업데이트를 수행합니다.
리스트를 렌더링할 때는 key가 중요합니다. 다음처럼 고유하고 안정적인 값을 사용하세요:
{todos.( (
))}
단방향 데이터 흐름은 데이터가 부모에서 자식으로 전달되고, 자식은 콜백을 통해 변경을 요청하는 방식입니다.
이 패턴은 경계를 명확하게 해 줍니다: “이 데이터를 누가 소유하고 있는가?”라는 질문에 대부분은 "가장 가까운 공통 부모"라고 답할 수 있습니다. 디버깅도 숨겨진 변경을 추적하는 대신 상태가 어디에 저장되었는지를 찾는 일이 됩니다.
컴포지션은 여러 작은, 집중된 조각을 조합해서 화면을 만드는 방식입니다. 상속 기반 접근 대신 다음 같은 패턴을 자주 봅니다:
PageShell 같은 레이아웃 컴포넌트(헤더/사이드바/푸터)Stack/Grid 같은 간격/정렬 컴포넌트Card 같은 프레임 컴포넌트이들은 보통 을 받아 페이지가 내부 내용을 정하도록 합니다. 필요할 때는 , , , 같은 슬롯 비슷한 prop을 통해 유연성을 제공합니다. 상속은 깊어지면 관리가 어렵지만 컴포지션은 더 예측 가능하고 유지보수하기 쉽습니다.
성장하는 React 앱에서 상태 관리는 보통 이렇게 진행됩니다:
선택은 유행이 아니라 앱 복잡도와 팀 필요에 따라 해야 합니다.
컴포넌트 경계가 명확하면 테스트와 성능 계획이 아키텍처 차원에서 이뤄질 수 있습니다.
테스트 관점:
성능 관점에서는 구조적 결정이 중요합니다: 코드 스플리팅, 메모이제이션(필요할 때), 레이지 로딩 같은 전략을 통해 초기 로드를 작게 하고 자주 재렌더되는 부분의 비용을 줄입니다.
주의할 점은 과도한 분해(너무 많은 작은 컴포넌트), prop 드릴링, 그리고 명확하지 않은 상태 소유권입니다. 자동 생성 코드나 스캐폴딩을 쓸 때 이 문제는 더 빨리 드러납니다. 핵심은 상태 소유권을 명확히 하고 컴포넌트 API를 작게 유지하는 것입니다.
서버 컴포넌트, 메타 프레임워크, 더 나은 툴링이 React 앱 전달 방식을 계속 진화시킬 것입니다. 하지만 변하지 않는 교훈은 다음과 같습니다: 상태와 소유권, 조합 가능한 UI 블록을 중심으로 설계하고, 그 다음에 테스트와 성능을 자연스럽게 따라오게 하라는 점입니다.
더 깊은 구조적 결정은 /blog/state-management-react를 참조하세요.
항목이 재정렬되거나 삽입/삭제될 수 있다면 배열 인덱스를 피하세요. 잘못된 컴포넌트 인스턴스 재사용으로 입력값이 엉키는 등 이상한 동작이 발생할 수 있습니다.
childrentitlefooterrenderRowemptyState