
클래스 컴포넌트와 함수 컴포넌트의 본질적 차이 및 리액트 훅의 작동 원리
핵심 요약
클래스 컴포넌트와 함수 컴포넌트는 근본적으로 다른 패러다임을 가진다. 클래스는 인스턴스 기반의 객체 지향적 접근이고, 함수는 선언적이고 함수형 프로그래밍 접근이다. 리액트 훅은 클로저와 연결 리스트를 활용한 정교한 메커니즘으로 함수 컴포넌트에서도 상태와 생명주기 기능을 제공하여, 클래스 컴포넌트의 복잡성 문제들을 해결했다.
1. 클래스 컴포넌트 vs 함수 컴포넌트: 본질적 차이
1.1 메모리 모델과 인스턴스 관리
클래스 컴포넌트
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // this 바인딩 필요
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>;
}
}
- 인스턴스 생성: 각 컴포넌트마다 클래스 인스턴스가 메모리에 생성됨12
- this 컨텍스트: 메서드들이 인스턴스에 바인딩되어야 함
- 상태 관리:
this.state
객체에 모든 상태가 중앙집중식으로 저장 - 생명주기: 명시적인 생명주기 메서드들(
componentDidMount
,componentDidUpdate
등)
함수 컴포넌트
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
- 함수 호출: 렌더링할 때마다 단순히 함수가 호출됨23
- this 없음: JavaScript의 복잡한
this
바인딩 문제 완전 해결42 - 선언적 상태: 각 상태가 개별적으로 선언되고 관리됨
- 이펙트 기반:
useEffect
로 모든 사이드 이펙트를 통합 처리
1.2 상태 업데이트 메커니즘의 차이
클래스: 얕은 병합(Shallow Merge)
this.state = { name: 'John', age: 25, city: 'Seoul' };
this.setState({ age: 26 });
// 결과: { name: 'John', age: 26, city: 'Seoul' } - 기존 속성 유지
함수: 완전 대체(Complete Replacement)
const [user, setUser] = useState({ name: 'John', age: 25, city: 'Seoul' });
setUser({ age: 26 });
// 결과: { age: 26 } - 다른 속성들 사라짐!
// 올바른 방법: 수동 병합
setUser(prev => ({ ...prev, age: 26 }));
이는 useState
가 클래스의 setState
와 의도적으로 다르게 설계되었기 때문이다567.
2. 클래스 컴포넌트가 해결하지 못한 문제들
2.1 생명주기 메서드의 로직 분산
class DataComponent extends React.Component {
componentDidMount() {
// 관련 없는 로직들이 뒤섞임
this.fetchUserData(); // 데이터 페칭
this.startTimer(); // 타이머 시작
document.addEventListener('resize', this.handleResize); // 이벤트 리스너
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(); // 데이터 페칭 로직이 또 다른 곳에
}
}
componentWillUnmount() {
this.clearTimer(); // 정리 로직이 별도 메서드에 분산
document.removeEventListener('resize', this.handleResize);
}
}
문제점: 연관된 로직(데이터 페칭과 정리)이 서로 다른 생명주기 메서드에 흩어져 있어 유지보수가 어려움89.
2.2 컴포넌트 간 로직 공유의 복잡성
HOC 패턴의 래퍼 지옥(Wrapper Hell)
const EnhancedComponent = withAuth(
withTheme(
withLoading(
withUserData(
withErrorHandling(MyComponent)
)
)
)
);
Render Props의 중첩 지옥
<AuthProvider>
{auth => (
<ThemeProvider>
{theme => (
<DataProvider>
{data => (
<LoadingProvider>
{loading => (
<MyComponent auth={auth} theme={theme} data={data} loading={loading} />
)}
</LoadingProvider>
)}
</DataProvider>
)}
</ThemeProvider>
)}
</AuthProvider>
이런 패턴들은 DevTools에서 컴포넌트 트리가 복잡해지고 디버깅이 매우 어려워졌다891011.
3. 리액트 훅의 내부 작동 원리
3.1 클로저(Closure) 기반 상태 관리
훅은 JavaScript 클로저를 활용하여 상태를 기억한다31213:
// 단순화된 useState 구현
function useState(initialValue) {
let _value = initialValue; // 클로저로 보존되는 값
const state = () => _value; // 현재 값 반환
const setState = (newValue) => { // 값 업데이트
_value = newValue;
scheduleRerender(); // 리렌더링 예약
};
return [state, setState];
}
클로저의 핵심: 함수가 생성될 때의 렉시컬 환경을 기억하여, 외부 함수가 종료되어도 내부 변수에 계속 접근할 수 있다1314.
3.2 연결 리스트(Linked List)로 훅 순서 관리
실제 React에서는 각 컴포넌트의 훅들을 연결 리스트로 관리한다151617:
// 각 훅 객체 구조
const hook = {
memoizedState: null, // 현재 상태값
baseState: null, // 초기 상태값
baseQueue: null, // 업데이트 큐
queue: null, // 디스패치 큐
next: null // 다음 훅을 가리키는 포인터
};
연결 과정:
function mountWorkInProgressHook() {
const hook = createHook();
if (workInProgressHook === null) {
// 첫 번째 훅
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 후속 훅들을 연결
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
이 구조 때문에 **“훅은 항상 최상단에서 호출”**해야 한다는 규칙이 생겼다1816. 조건문 안에서 호출하면 연결 리스트의 순서가 망가진다.
3.3 Dispatcher 시스템: Mount vs Update
React는 컴포넌트의 생명주기에 따라 다른 훅 구현체를 사용한다1920:
// 최초 렌더링 시
const HooksDispatcherOnMount = {
useState: mountState,
useEffect: mountEffect,
useCallback: mountCallback,
// ...
};
// 업데이트 시
const HooksDispatcherOnUpdate = {
useState: updateState,
useEffect: updateEffect,
useCallback: updateCallback,
// ...
};
컴포넌트 렌더링 과정:
function renderWithHooks(current, workInProgress, Component) {
if (current === null) {
// 첫 렌더링: Mount용 dispatcher 사용
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
} else {
// 재렌더링: Update용 dispatcher 사용
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
}
const children = Component(props); // 컴포넌트 함수 실행
return children;
}
4. 훅이 해결한 구체적 문제들
4.1 로직 재사용의 단순화
이전 (HOC/Render Props):
// 복잡한 래퍼 구조
const withMouseTracking = (Component) => {
return class extends React.Component {
// 마우스 추적 로직...
render() {
return <Component {...this.props} mouse={this.state.mouse} />;
}
};
};
이후 (커스텀 훅):
// 깔끔한 로직 추출
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// 사용
function MyComponent() {
const mouse = useMouse();
return <div>Mouse: {mouse.x}, {mouse.y}</div>;
}
4.2 관련 로직의 그룹화
이전:
class Timer extends React.Component {
componentDidMount() {
this.startTimer(); // 로직 A
this.fetchData(); // 로직 B
}
componentWillUnmount() {
this.clearTimer(); // 로직 A (분리됨)
this.cancelRequests(); // 로직 B (분리됨)
}
}
이후:
function Timer() {
// 타이머 관련 로직이 한 곳에
useEffect(() => {
const timer = setInterval(() => {/* ... */}, 1000);
return () => clearInterval(timer); // 설정과 정리가 함께
}, []);
// 데이터 관련 로직이 한 곳에
useEffect(() => {
const request = fetchData();
return () => request.cancel(); // 설정과 정리가 함께
}, []);
}
4.3 성능 최적화 내장
훅은 React의 미래 최적화 기법과 호환성이 좋다8:
- 컴포넌트 폴딩: 불필요한 렌더링 최적화
- Concurrent Mode: 비동기 렌더링 지원
- 코드 분할: 번들 크기 최적화
5. 실제 동작 예시: useState의 내부 구현
// React 내부의 실제 구현 (단순화)
let workInProgressHook = null;
let currentlyRenderingFiber = null;
function useState(initialState) {
return useReducer(
(state, action) => typeof action === 'function' ? action(state) : action,
initialState
);
}
function useReducer(reducer, initialState) {
const hook = updateWorkInProgressHook();
if (currentlyRenderingFiber.alternate === null) {
// 첫 렌더링: 초기값 설정
hook.memoizedState = initialState;
hook.queue = { pending: null };
}
const dispatch = (action) => {
const update = { action, next: null };
// 업데이트를 큐에 추가
const queue = hook.queue;
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 리렌더링 스케줄링
scheduleWork(currentlyRenderingFiber);
};
return [hook.memoizedState, dispatch];
}
결론
리액트 훅은 단순한 문법 설탕이 아니라, 클로저와 연결 리스트를 기반으로 한 정교한 시스템이다. 클래스 컴포넌트의 근본적 문제들—복잡한 this
바인딩, 로직 분산, 래퍼 지옥, 재사용성 부족—을 해결하면서도, 더 직관적이고 함수형 프로그래밍 패러다임에 맞는 개발 경험을 제공한다.
훅의 **“마법”은 실제로는 JavaScript의 기본 개념들(클로저, 연결 리스트)**을 활용한 정교한 엔지니어링의 결과물이며, 이를 통해 React는 더욱 선언적이고 조합 가능한 컴포넌트 시스템으로 발전할 수 있었다.
Footnotes
-
https://www.geeksforgeeks.org/blogs/differences-between-functional-components-and-class-components/ ↩
-
https://www.freecodecamp.org/news/function-component-vs-class-component-in-react/ ↩ ↩2 ↩3
-
https://www.fullstack.com/labs/resources/blog/behind-the-scenes-react-hooks-api ↩ ↩2
-
https://www.geeksforgeeks.org/reactjs/limitations-of-class-components-in-react/ ↩
-
https://stackoverflow.com/questions/77413837/does-setstate-in-react-shallow-merge-or-completely-replace-the-state ↩
-
https://www.linkedin.com/pulse/6-common-mistakes-when-using-react-usestate-hook-code-akshay-kaushik-spn0c ↩
-
https://qirolab.com/posts/react-state-management-usestate-hook-vs-class-setstate-1719313287 ↩
-
https://www.dhiwise.com/post/leveraging-react-render-props-for-flexible-component-composition ↩ ↩2
-
https://sunscrapers.com/blog/how-advanced-react-patterns-changed/ ↩
-
https://patterns-dev-kr.github.io/design-patterns/hooks-pattern/ ↩
-
https://incepter.github.io/how-react-works/docs/react-dom/how.hooks.work/ ↩
-
https://dev.to/wuz/linked-lists-in-the-wild-react-hooks-3ep8 ↩ ↩2