우리 프론트엔드 개발자들에게 어쩌면 JS보다 더 친숙하게 느껴지는 React.
매일같이 마주하지만, 막상 "왜 React인가?"라는 질문에는 막연한 대답만 떠오를지도 모른다.
이번 글에서는 익숙한 React에 대해 잠시 멈춰 서서, 처음으로 돌아가 궁금증을 던져보려 한다.
React가 왜 등장했는지, 프레임워크가 아닌 라이브러리라는 말의 의미는 무엇인지, 그리고 우리가 React를 왜 쓰고 있는지 말이다.
2006년, John Resig이 jQuery를 발표하였다.
당시 DOM 조작은 복잡하고 번거로웠지만, jQuery는 $()와 같은 간단한 API로 이를 극적으로 단순화시켰다.
jQuery는 폭발적인 인기를 얻었고, 오랫동안 사실상의 표준처럼 사용되었다.
웹앱이 점점 복잡해지면서, SPA(Single Page Application) 개념이 주목받기 시작하였다.
SPA는 전체 페이지를 새로고침하지 않고 필요한 부분만 바꿔주는 방식으로 사용자 경험을 향상시켰다.
이 흐름 속에서 등장한 것이 Backbone.js와 AngularJS이다.
Backbone.js는 2010년 등장한 라이브러리로, MVC 아키텍처를 통해 코드를 구조화할 수 있게 해주었다.
모델, 뷰, 라우터 등으로 역할을 나누어 보다 깔끔한 코드 구성이 가능해졌다.
AngularJS는 Google이 만든 프레임워크로, 양방향 데이터 바인딩과 의존성 주입(DI)이라는 강력한 기능을 제공하였다.
템플릿과 모델이 서로 연결되어 있어 UI 자동 갱신이 가능해졌고, 복잡한 앱도 보다 간편하게 구성할 수 있었다.
그러나 이들 역시 완전히 jQuery의 의존에서 벗어나지 못하였다.
DOM을 직접 다루는 방식의 한계는 여전히 존재했고, UI 상태와 DOM의 일관성을 유지하는 일은 점점 더 어려워졌다.
2013년, Facebook은 복잡한 UI를 효율적으로 관리하기 위해 React라는 새로운 라이브러리를 공개하였다.
React는 기존 도구들과는 전혀 다른 철학을 기반으로 하였고, 그로 인해 빠르게 전 세계 개발자들의 주목을 받았다.
Facebook은 물론 Airbnb, Netflix, Dropbox, Twitter 등 수많은 글로벌 기업들이 React를 도입하게 되었다.
📌 "프레임워크가 너의 코드를 호출한다."
예시
Angular를 쓴다면, 컴포넌트 생성, 라우팅, DI, 데이터 바인딩 등 모든 구조를 Angular가 미리 정해놓고, 우리는 그 규칙대로 작성해야 한다.
📌 "내가 라이브러리를 호출한다."
예시
React는 컴포넌트만 담당한다. 라우팅이 필요하면 react-router, 전역 상태관리는 zustand나 recoil처럼 개발자가 따로 추가해서 조합해야 한다.
그러나 우리는 이미 프레임워크를 써본 경험이 있을 수 있다.
1. 라우팅을 내가 만들지 않아도 된다
React에선 라우팅을 내가 react-router-dom으로 직접 구성해야 한다.
하지만 Next.js에서는 파일 이름이 URL이 된다.
/app
└─ page.tsx → /
└─ about/page.tsx → /about
└─ blog/[id]/page.tsx → /blog/:id
이렇게 폴더만 만들어두면 Next.js가 알아서 라우팅을 설정해준다.
프레임워크가 라우팅 흐름을 주도한다.
2. 서버 사이드 렌더링(SSR), 정적 사이트 생성(SSG)도 자동 지원
React만 쓸 때는 서버사이드 렌더링을 하려면 복잡한 설정을 해야 했지만,
Next.js는 그냥 이렇게 함수만 쓰면 된다:
export async function getServerSideProps() {
const res = await fetch('https://api.example.com/data')
const data = await res.json()
return { props: { data } }
}
3. 프로젝트 구조와 규칙이 이미 정해져 있다
Next.js에서는 app/, pages/, public/, api/ 같은 폴더의 역할이 정해져 있다.
이 규칙을 어기면 작동하지 않는다.
예를 들어:
/app/layout.tsx: 모든 페이지 공통 레이아웃
/app/page.tsx: 홈 페이지
/app/contact/page.tsx: /contact 라우트
👉 등등등 이렇듯 프레임워크를 사용하면 개발자는 정해진 규칙 안에서만 코드를 짠다.
프레임워크가 앱의 흐름을 먼저 정의해 놓고, 거기에 끼워 넣는 방식이다.
React는 Angular처럼 전체 아키텍처를 통제하지 않는다.
라우팅, 전역 상태 관리, 데이터 패칭 등은 모두 외부 도구에 의존하며, 필요한 기능만 가져와 조합하는 유연한 구조를 가지고 있다.
이로 인해 React는 가볍고 빠르게 배울 수 있으며, 개발자의 창의력을 살릴 수 있는 여지를 많이 남겨준다.
기존 방식은 DOM을 개발자가 직접 조작해야 했기 때문에, 어떤 상태에서 어떤 UI가 나와야 하는지를 추적하기가 어려웠다.
하지만 React는 상태(state)가 바뀌면, 그에 맞는 UI를 자동으로 다시 그려주는 방식을 택하였다.
React는 UI = f(state)라는 철학을 가진다.
즉, 상태(state)에 따라 UI가 결정되며, 개발자는 UI를 선언적으로 표현하기만 하면 된다.
이전처럼 "이 버튼이 눌리면 이 텍스트를 바꿔라"가 아니라, “이 상태일 땐 이렇게 보여줘” 라고 말하면 된다.
const [isOn, setIsOn] = useState(false)
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? "켜짐" : "꺼짐"}
</button>
)
이 코드는 이렇게 말하는 것과 같다:
"isOn이 true면 ‘켜짐’을, 아니면 ‘꺼짐’을 버튼에 보여줘."
어떻게 버튼 텍스트를 바꿀지, DOM을 어떻게 조작할지는 내가 신경 쓰지 않는다.
React가 알아서 바꿔준다.
“어떻게 보여줄지를 내가 직접 하나하나 명령해서 작성한다.”
jQuery 같은 도구가 명령형 UI에 가깝다.
상태가 바뀌면, DOM도 내가 직접 조작해야 한다.
let isOn = false
$('#btn').on('click', function () {
isOn = !isOn
if (isOn) {
$('#btn').text('켜짐')
} else {
$('#btn').text('꺼짐')
}
})
이 코드는 이렇게 말하는 것과 같다:
“버튼을 클릭하면 상태를 바꾸고, 텍스트도 내가 직접 바꿔줄게.”
버튼 상태와 UI 변경이 완전히 분리되어 있다.
개발자가 일일이 관리해줘야 하니까 버그 나기 쉽고, 복잡한 앱일수록 유지보수가 어렵다.
| 구분 | 선언형 UI | 명령형 UI |
|---|---|---|
| 방식 | 무엇을 보여줄지 선언 | 어떻게 보여줄지 직접 명령 |
| 코드 역할 | 상태만 정의하면 UI는 자동 | 상태 + DOM 조작 모두 직접 |
| 생산성 | 높음 | 낮음 (반복 코드 많음) |
| 예시 | React, Vue | jQuery, Vanilla JS |
선언형 UI는 최종 모습을 말하고,
명령형 UI는 구현 방법을 하나하나 말한다.
React는 상태가 변경되면 전체 UI를 다시 렌더링하는 대신, 메모리 상의 Virtual DOM에서 변경 사항을 계산하고,
최소한의 차이만 실제 DOM에 반영한다.
<div>Hello {this.props.name}</div>
<div>I am {this.state.chatName}</div> //얘만 변경
위와 같은 컴포넌트에서 chatName만 변경되었다면, React는 이 차이만 찾아서 진짜 DOM에 반영한다.
브라우저에게 DOM 변경은 매우 무거운 작업이기 때문에, 이런 방식은 성능상 큰 이점을 가진다.
React에서 UI는 모두 컴포넌트 단위로 구성된다. 하나의 컴포넌트는 자체적인 로직과 상태를 가지며, 재사용이 가능하다. 이 구조 덕분에 유지보수와 협업이 쉬워지고, UI의 복잡도를 잘 분산시킬 수 있다. Dan Abramov는 React의 목표가 성능보다 “유지 가능한 앱”에 있다고 말한 바 있다. React로 개발한다는 것은 곧 작은 컴포넌트를 조합하여 하나의 앱을 만들어가는 일이다.
React는 데이터를 한 방향으로만 흘러가게 설계되었다.
“상위 컴포넌트 → 하위 컴포넌트로만 데이터가 흐른다.”
(이벤트 콜백을 통해 상위에게 '값 바꿔줘' 라는 요청은 보낼 수 있다)
상태(state)는 보통 부모가 갖고 있고, 하위 컴포넌트는 그 상태를 props로 전달받는다.
하위 컴포넌트는 직접 상태를 바꿀 수 없다.
단지 부모에게 “이거 바꿔줘”라고 신호만 보낸다.
상태(state)는 오직 Parent에 있음.
Child는 읽기만 하고, 바꾸고 싶을 때는 "setText" 요청만 보냄.
function Parent() {
const [text, setText] = useState('Hello')
return <Child value={text} onChange={setText} />
}
function Child({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
)
}
text는 부모가 관리하고,
자식은 그냥 그 값을 보여주고 바뀌면 부모에게 알린다.
👉 데이터 흐름: Parent → Child → Parent에게 이벤트 전달
“데이터가 컴포넌트 안에서 상호작용하며 양쪽에서 자동으로 갱신된다.”
🧾 예시: AngularJS의 양방향 바인딩
<input ng-model="name">
<p>Hello {{ name }}</p>
사용자가 입력을 바꾸면 name도 자동으로 바뀌고,
반대로 name이 코드에서 바뀌면 <input>의 값도 자동 갱신된다.
편리하긴 하지만, 앱이 커지면 데이터가 어디서 바뀌었는지 추적하기 어려워지고
예상치 못한 버그가 생기기도 한다.
| 구분 | 단방향 데이터 흐름 | 양방향 바인딩 |
|---|---|---|
| 데이터 흐름 | 상위 → 하위 (한 방향) | 양방향 (상호 연결) |
| 상태 변경 방식 | 부모에서만 상태를 변경 | 뷰와 모델이 자동 동기화 |
| 예측 가능성 | 높음 (흐름이 명확함) | 낮음 (의도치 않게 바뀔 수 있음) |
| 대표 예시 | React | AngularJS (1.x) |
데이터 흐름이 예측 가능하다.
디버깅이 쉽다 (어디서 바뀌었는지 추적이 명확하다).
상태가 한 곳에 모여 있으니 관리가 편하다.
복잡한 앱일수록 이 구조가 진가를 발휘한다.
단방향 흐름은 "내가 줄게, 네가 바꾸고 싶으면 나한테 말해.
"양방향 흐름은 "너랑 나랑 서로 알아서 자동으로 바꾸자."
React는 통제 가능성을 선택했고, 그래서 복잡한 앱에서도 안정적으로 동작할 수 있다
이러한 특성들 덕분에 React는 빠르게 주목받았고, 현재는 가장 인기 있는 프론트엔드 라이브러리 중 하나가 되었다.