React의 선언적 특성은 특정 개인이 만든 슬로건이 아니라, React 자체의 본질적 특성을 나타내는 공식적인 표현이다.
📖 참고 | React GitHub 저장소, React - Wikipedia
React를 배우다 보면 "React는 선언적이다"라는 말을 자주 듣게 된다.
처음 들으면 "선언"이라고 하면 "나는 인간이다!" 처럼 그냥 말하는 것 아닌가 하는 생각이 든다...
하지만 프로그래밍에서 말하는 "선언적"은 조금 다른 의미다.
아래 예시로 살펴보자.
엄마: "방 청소해!"
아이: "어떻게요?"
엄마: "1단계: 옷장에서 옷 정리해
2단계: 책상 위 정리해
3단계: 바닥에 떨어진 것들 주워
4단계: 청소기 돌려
5단계: 걸레질해"
엄마: "방을 깨끗하게 해줘"
아이: (알아서 방법을 선택해서 깨끗하게 만듦)
이처럼 선언적 프로그래밍은 과정(How)이 아니라 결과(What)에 집중하는 방식이다.
이 원리를 다른 상황에 대입해 보면, 선언적 사고가 얼마나 직관적인지 더 잘 보인다.
명령형 : "500m 앞에서 직진 → 우회전 → 신호등에서 좌회전 → 주차장 입구에서 우회전"
선언적 : "강남역으로 가줘" (네비가 알아서 최적 경로 계산)
즉, "최종 목적지"만 말하는 것이 선언적 접근이고, 세세한 경로까지 일일이 지시하는 것이 명령형 접근이다.
그렇다면 프로그래밍 언어와 UI 구성에선 어떻게 드러날까?
명령형: "이렇게 단계별로 만들어줘"
const button = document.createElement('button');
button.style.background = 'blue';
button.style.color = 'white';
button.textContent = '클릭하세요';
document.body.appendChild(button);
선언적: "이런 모습이었으면 좋겠어!"
<button style="background: blue; color: white;">
클릭하세요
</button>
HTML 자체가 "이런 모습이었으면 좋겠다"라고 결과만 표현하는 대표적인 선언적 언어다.
React도 바로 이런 HTML의 선언적 성격을 확장해서 사용한다.
// DOM을 직접 조작 - 단계별로 명령
function updateCounter() {
const counterElement = document.getElementById('counter');
const currentValue = parseInt(counterElement.textContent);
const newValue = currentValue + 1;
counterElement.textContent = newValue;
if (newValue > 10) {
counterElement.style.color = 'red';
} else {
counterElement.style.color = 'black';
}
}
// 원하는 UI 모습만 그려놓음
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<span style={{ color: count > 10 ? 'red' : 'black' }}>
{count}
</span>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
}
// "어떻게" 에러를 처리할지 일일이 명령
function Child({ errorValue, isError, setIsError }) {
try { // 명령 1: 에러 감지해
if (!isError) { // 명령 2: 상태 확인해
return <div>{errorValue.toPrecision()}</div>; // 명령 3: 정상 렌더링해
}
return <p>Error 발생!</p>; // 명령 4: 에러 UI 렌더링해
} catch (error) { // 명령 5: 에러 잡아
setIsError(true); // 명령 6: 상태 바꿔
}
}
// "무엇을" 원하는지만 선언
<ErrorBoundary fallback={<p>Error 발생!</p>}>
<Child errorValue={errorValue} /> {/* 이런 모습이었으면 좋겠어! */}
</ErrorBoundary>
지금까지 본 예시들로는 React의 선언적 특성이 "편리함" 정도로만 느껴질 수 있다. 하지만 실제로는 훨씬 더 깊은 철학적 배경이 있다.
실무에서는 이런 복잡한 상황들을 마주한다:
// 이런 상황이 발생한다면?
function ComplexComponent() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [sortBy, setSortBy] = useState('name');
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(1);
// 이 상태들이 복잡하게 얽혀있을 때...
// 언제 무엇을 업데이트해야 할까?
// 순서는? 의존성은? 부작용은?
}
명령형 사고로 접근하면: "사용자가 검색어를 바꿨으니 → 필터를 초기화하고 → 페이지를 1로 리셋하고 → 로딩 상태로 만들고 → API를 호출하고 → 결과에 따라 에러나 성공 상태로 변경하고..."
선언적 사고로 접근하면: "이런 상태일 때는 이런 모습이면 돼"
이 차이가 왜 중요한지, 그리고 이것이 함수형 프로그래밍과 어떤 관련이 있는지 알아보자.
React 창시자 Jordan Walke와 Facebook팀이 실제로 직면한 문제는 다음과 같았다:
Facebook Ads의 "Cascading Updates" 문제: Facebook Ads 앱이 기능이 늘어나면서 하나의 변화가 연쇄적으로 다른 부분들을 업데이트해야 하는 상황이 발생했다. 이런 연쇄 업데이트를 추적하고 관리하기가 점점 어려워졌다.
코드 유지보수의 어려움: 팀 규모가 커지고 앱이 복잡해지면서 코드 관리가 어려워졌다. Jordan Walke의 말에 따르면 "더 많은 사람이 필요했지만, 동시에 그것이 개발 속도를 느리게 만들었다"고 한다.
DOM 업데이트의 복잡성: Jordan Walke는 "DOM 업데이트가 UI 개발에서 가장 어려운 작업"이라고 말했다. 매번 DOM을 수동으로 조작하는 것이 버그를 만들기 쉽고 비효율적이었다.
개인적 철학의 차이: Jordan Walke는 "처음 프로그래밍을 배울 때부터 MVC 스타일의 data binding과 mutation이 직관적이지 않다고 느꼈다"고 회고했다. 그는 자신이 "이상한 프로그래머"라고 생각했지만, 나중에 함수형 프로그래밍을 배우고 나서야 자신의 접근 방식이 올바른 방향이었음을 깨달았다.
이런 실제 경험을 바탕으로 React팀은 "DOM을 처음부터 다시 렌더링하는 것처럼 생각하되, 실제로는 변경된 부분만 효율적으로 업데이트하자"는 아이디어를 개발했다. 이것이 바로 Virtual DOM과 선언적 프로그래밍 방식의 출발점이었다.
UI = f(state)
즉, 특정 상태가 주어지면 UI는 항상 같은 모습이어야 한다는 철학이다. 이것이 React의 선언적 특성의 진짜 의미이고, 함수형 프로그래밍과 연결되는 지점이다.
📖 참고 | Jordan Walke Q&A (2017) - Reactiflux, The History of React.js on a Timeline - RisingStack
React의 선언적 특성을 더 깊이 이해하려면 함수형 프로그래밍의 핵심 개념을 알아야 한다.
| 구분 | 특징 | 프로그래밍 예시 | 일상생활 예시 |
|---|---|---|---|
| 액션(Action) | 부수효과 있음, 실행 시점·환경에 따라 결과 달라짐 | fetch()/HTTP 요청, DOM 조작(appendChild) | 전화 주문하기, 택배 보내기 |
| 계산(Calculation) | 순수함수, 입력이 같으면 항상 같은 결과 | 수학 연산, 문자열 처리, (불변 기반) 배열 정렬/포맷팅 | 계산기 계산, 레시피 절차 이해, 문장 번역 규칙 적용 |
| 데이터(Data) | 불변값/사실, 그 자체로 의미 | 숫자, 문자열, 객체/레코드 | 이름, 나이, 주소, 재료 목록 |
→ 언제 하느냐, 어떤 상황이냐가 중요!
→ 입력이 같으면 결과도 항상 같음!
→ 행동이 아니라 사실/정보 자체!
💡 참고: 이 액션/계산/데이터 구분은 함수형 프로그래밍을 쉽게 이해하기 위해 단순화한 모델이다.
실제 함수형 프로그래밍 학계·커뮤니티에서 통용되는 개념과 연결하면 아래와 같다
| 책에서의 구분 | 의미 | 함수형 프로그래밍 정식 개념과 대응 |
|---|---|---|
| 계산 (Calculation) | 입력이 같으면 항상 같은 결과를 내는 순수한 연산 | 순수 함수(Pure Function), 참조 투명성(Referential Transparency) |
| 액션 (Action) | 실행 시점이나 외부 환경에 따라 달라지고, 외부 세계에 영향을 주는 행위 | 부수효과(Side Effect) |
| 데이터 (Data) | 그 자체로 의미 있는 값, 변하지 않는 정보 | 불변성(Immutability) |
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function UserProfile({ user }) {
console.log('렌더링됨!'); // 액션: 부수효과
const randomId = Math.random(); // 액션: 실행할 때마다 다름
return (
<div id={randomId}>
<h1>{user.name}</h1>
</div>
);
}
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// 액션을 명확히 분리
useEffect(() => {
// API 호출: 액션
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
// 컴포넌트 자체는 순수한 계산
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
function updateUserDisplay(userId) {
const user = fetchUser(userId); // 액션
const displayName = user.name.toUpperCase(); // 계산
const element = document.getElementById('user'); // 액션
element.textContent = displayName; // 액션
console.log('업데이트 완료'); // 액션
}
// 선언적: 역할이 명확히 분리됨
function UserDisplay({ user }) { // 데이터 받음
const displayName = user.name.toUpperCase(); // 계산
return <div>{displayName}</div>; // UI 선언 (계산)
}
// 액션은 상위에서 처리
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(1).then(setUser); // 액션
}, []);
return <UserDisplay user={user} />;
}
// 이 컴포넌트는 props가 같으면 항상 같은 결과
function ProductCard({ product, isLoggedIn }) {
return (
<div className="card">
<h3>{product.name}</h3>
<p>{product.price}</p>
{isLoggedIn && <button>장바구니 추가</button>}
</div>
);
}
명령형에서는 "이 함수를 호출하면 무슨 일이 일어날까?"라는 불안감이 있었다. 전역변수를 바꿀 수도 있고, DOM을 조작할 수도 있고, API를 호출할 수도 있으니까.
하지만 React 컴포넌트는 "같은 props → 같은 UI"라는 단순한 규칙이 있어서 안심하고 사용할 수 있다.
// 버그가 생겼을 때
function ShoppingCart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return (
<div>
<h2>총 금액: {total}</h2> {/* 여기가 이상하다면? */}
{items.map(item => <div key={item.id}>{item.name}</div>)}
</div>
);
}
명령형이었다면: "total이 이상하네... 언제 어디서 바뀐 거지? 이 함수 저 함수 다 뒤져봐야겠다"
React에서는: "total이 이상하다면 items가 이상한 거다. items를 전달하는 곳만 확인하면 돼"
// 이 컴포넌트를 테스트하려면?
function UserGreeting({ user, currentTime }) {
const timeOfDay = currentTime.getHours() < 12 ? '오전' : '오후';
const message = `${timeOfDay}입니다, ${user.name}님!`;
return <div>{message}</div>;
}
// 테스트는 이렇게 간단
test('아침 시간 인사 메시지', () => {
const user = { name: '김철수' };
const morningTime = new Date('2023-01-01 09:00:00');
const result = render(<UserGreeting user={user} currentTime={morningTime} />);
expect(result).toContain('오전입니다, 김철수님!');
});
명령형에서는 "이 함수를 테스트하려면 DOM을 만들고, 전역변수를 설정하고, API를 모킹하고..." 복잡했지만, React에서는 단순히 props 넣고 결과 확인하면 끝이다.
React의 선언적 특성은 단순히 코드를 편리하게 만드는 것을 넘어, 팀 단위의 개발과 비즈니스 성과에 직접적인 영향을 미친다.
명령형 방식은 하나의 기능 변경이 다른 부분에 예상치 못한 부작용을 일으킬 수 있어, 코드 리뷰 시 전체적인 흐름을 파악해야 하는 어려움이 있다. 하지만 React 컴포넌트는 'props가 같으면 항상 같은 UI를 반환한다'는 규칙이 있어, 리뷰어는 해당 컴포넌트 내부의 로직과 의도된 UI만 집중적으로 확인할 수 있다.
명령형 방식의 컴포넌트는 전역 상태나 외부 요인에 영향을 받을 수 있어 재사용하기가 까다롭다. 반면, React의 순수한 컴포넌트는 입력(props)에 대한 출력(UI)이 항상 일정하므로, 다른 프로젝트나 기능에서 안심하고 재사용할 수 있다. 이는 개발의 생산성을 크게 높여준다.
선언적 프로그래밍은 개발자가 "어떻게(how)" DOM을 조작할지에 대한 기술적 세부사항에 매달리지 않고, "무엇을(what)" 만들지에만 집중하게 해준다. 이로 인해 불필요한 고민과 시간 낭비가 줄어 개발 속도가 빨라진다. 또한, 예측 가능한 코드 덕분에 신규 개발자가 프로젝트에 빠르게 적응할 수 있으며, 예상치 못한 버그 발생률이 현저히 낮아져 장기적인 유지보수 비용을 절감할 수 있다.
React의 선언적 특성은 근본적으로 소프트웨어 개발 방식의 패러다임 변화를 가져왔다.
명령형에서는 개발자가 "컴퓨터가 어떻게 할지"를 지시하는 관리자 역할이었다면, 선언적에서는 "무엇을 원하는지"를 표현하는 설계자 역할로 바뀌었다.
이는 단순히 코드가 깔끔해지는 것을 넘어서, 더 안전하고, 예측 가능하며, 협업하기 좋은 코드를 작성할 수 있게 해주는 철학적 전환이다.
React가 "선언적"이라는 것은 결국 "개발자가 본질적인 문제에 집중할 수 있도록 해주는 도구"라는 의미이며, 이것이 React가 현대 웹 개발의 표준이 된 핵심 이유이다.
📖 참고 | Thinking in React
지금까지 "React는 선언적이다"라는 한 문장에 담긴 깊은 의미를 살펴보았다.
"선언적"이라는 말은 이제 더 이상 헷갈리지 않을 것이다. "나는 이런 모습이 되기를 원한다"라고 컴퓨터에게 최종 결과를 선언하는 것. 그것이 바로 React 개발의 핵심이며, 우리가 이 패러다임을 따라야 하는 이유다.
📖 참고 | The Reason Why React Was Created - DEV Community, The History of React Through Code - Playful Programming, The History of React.js - Zeeshan Ali Blog