상태관리는 현대 프론트엔드 개발에서 가장 중요한 개념 중 하나라고 생각한다.
특히 React와 Vue를 사용할 때 "단방향"과 "양방향" 상태관리의 차이를 이해하는 것은 필수이기 때문에
해당 개념에 대해 공부하고 정리한 내용을 기록해보려고 한다.
상태관리란?
먼저 "상태(State)"가 뭔지 정의해보자
상태 = 애플리케이션이 가지고 있는 모든 데이터
예:
- 로그인한 사용자 정보
- 장바구니의 상품들
- 좋아요 수
- 다크모드 ON/OFF
상태관리는 이 데이터들이 어떻게 변경되고, 어떤 경로를 통해 변경되며, 어떻게 컴포넌트들에게 공유되는지를 관리하는 것이다.
여기서 중요한것은 이 상태관리를 어떻게 할 것이냐 이다.
단방향 vs 양방향: 개념부터 이해하기
상태관리는 크게 두 가지 철학으로 나뉜다. 이것을 먼저 이해해야 React와 Vue의 차이가 명확해진다.
단방향 상태관리란?
상태 변경이 일관된 한 방향으로만 흐른다.
User Action
↓
setState / Dispatch 호출
↓
상태 변경
↓
View 업데이트
↓
(다시 처음으로)
특징:
✅ 명확한 흐름
✅ 상태 변경을 추적하기 쉬움
✅ 예측 가능한 동작
❌ 코드가 많을 수 있음
양방향 상태관리란?
View와 State가 직접 연결되어 자동으로 동기화된다
User Input ↔ State ↔ View
(입력하면 자동으로 상태 변경, 상태 변경되면 자동으로 화면 업데이트)
특징:
✅ 코드가 간단함
✅ 자동 동기화
✅ 빠른 개발
❌ 추적이 어려울 수 있음
❌ 복잡해질 수 있음
왜 React와 Vue로 이걸 비교할까?
프론트엔드 프레임워크마다 기본 설계 철학이 다르다.
React: 엄격한 규칙 (단방향) → 복잡한 애플리케이션에 강하다.
Vue: 유연한 구조 (양방향) → 빠른 개발에 강하다.
때문에 위 두 프레임워크로 예시를 들어 단방향 & 양방향 상태관리를 공부해보려고 한다.
하지만 React, Vue 모두 라이브러리을 사용하면 양방향 & 단방향 패턴으로 변경 가능하다.
React
React의 기본 원칙
React는 부모에서 자식으로만 데이터가 흐른다는 원칙을 가진다.
Parent Component (상태 소유)
↓ (props 전달)
Child Component (상태 사용)
↓ (콜백 함수 호출)
Parent Component (상태 변경)
좋아요 기능을 예시로 들어보자
// ========== PARENT (상태를 소유) ==========
function App() {
// 1단계: 상태 선언
const [likes, setLikes] = useState(0);
// 5단계: 상태 변경 함수 정의
const handleLikeClick = () => {
setLikes(likes + 1);
};
return (
<div>
{/* 2단계: 자식에게 상태를 props로 전달 */}
<LikeButton
likes={likes}
onLikeClick={handleLikeClick}
/>
</div>
);
}
// ========== CHILD (상태를 받음) ==========
function LikeButton({ likes, onLikeClick }) {
return (
<div>
<p>❤️ {likes}</p>
{/* 3단계: 버튼 클릭 시 부모의 콜백 함수 호출 */}
<button onClick={onLikeClick}>
좋아요
</button>
</div>
);
}
User Click Button
↓
onLikeClick 실행 (자식에서 부모의 콜백 호출)
↓
setLikes(likes + 1) 실행 (부모의 상태 변경)
↓
App 컴포넌트 리렌더링
↓
새로운 likes 값이 props로 자식에게 전달
↓
LikeButton 리렌더링
↓
화면에 "❤️ 1" 표시
특징:
- 데이터는 부모 → 자식으로만 흐름
- 자식이 변경하고 싶으면 부모의 함수를 호출
- 부모가 상태를 변경
이런 단방향 상태관리에도 단점이 하나 있는데 그건 바로 Props Drilling 이다.
예를 들어 깊은 계층의 컴포넌트에서 상태가 필요한 경우를 통해 알아보자
// ========== 최상위 (App) ==========
function App() {
const [user, setUser] = useState({ name: "Kim" });
return (
<Level1 user={user} setUser={setUser} />
);
}
// ========== Level1 ==========
function Level1({ user, setUser }) {
// user, setUser를 안 쓰지만 다음 레벨로 전달
return (
<Level2 user={user} setUser={setUser} />
);
}
// ========== Level2 ==========
function Level2({ user, setUser }) {
// user, setUser를 안 쓰지만 다음 레벨로 전달
return (
<Level3 user={user} setUser={setUser} />
);
}
// ========== Level3 ==========
function Level3({ user, setUser }) {
// user, setUser를 안 쓰지만 다음 레벨로 전달
return (
<Level4 user={user} setUser={setUser} />
);
}
// ========== Level4 (드디어 필요!) ==========
function Level4({ user, setUser }) {
return (
<div>
<p>{user.name}</p>
<button onClick={() => setUser({ name: "Lee" })}>
이름 변경
</button>
</div>
);
}
위 경우의 문제점은 아래와 같다.
- Level1, Level2, Level3은 user를 사용하지 않는데도 props를 받고 전달해야 함
- 중간에 props를 놓치기 쉬움
- 컴포넌트 구조가 복잡해짐
Level 1,2,3,4 로 컴포넌트를 실제 프로젝트로 연결해본다면 다음과 같을 것 이다.
🛒 쇼핑몰 앱을 만드는데,
App (맨 위에서 사용자 정보 "김철수"를 보유)
│
├─ Header
│ └─ UserProfile ← "김철수" 정보 필요 (프로필 표시)
│
└─ MainContent
│
└─ ProductSection
│
└─ ProductCard
│
└─ BuyButton ← "김철수" 정보 필요 ("김철수님이 구매했습니다" 표시)
"김철수" 정보를 BuyButton까지 전달하려면:
App → MainContent (안 쓰지만 받음)
→ ProductSection (안 쓰지만 받음)
→ ProductCard (안 쓰지만 받음)
→ BuyButton (드디어 쓸 곳!)
중간의 MainContent, ProductSection, ProductCard는
"김철수"를 아예 안 쓰는데도
props를 받아야 하고, 그걸 아래로 전달해야 함.
이걸 "Props Drilling" 라고 부름
단방향의 장점
1. 데이터 흐름이 명확 - 어디서 상태가 변경되는지 추적 쉬움
2. 버그 찾기 쉬움 - 한 방향이어서 영향 범위가 명확
3. 예측 가능 - "부모가 변경 → 자식이 업데이트" 패턴만 있음
단방향의 문제점
1. Props Drilling - 깊은 계층에서 데이터 전달이 번거로움
2. 상태 끌어올리기 - 공통 상태가 필요할 때마다 상위로 올려야 함
이런 단방향 상태관리의 문제점을 해결하기 위해 React Context API를 활용할수있다.
// ========== UserContext.js ==========
import { createContext } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) {
const [user, setUser] = useState({
id: 1,
name: "김철수",
email: "kim@example.com"
});
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
// ========== App.jsx ==========
function App() {
return (
<UserProvider>
<Header />
<MainContent />
</UserProvider>
);
}
// ========== Header.jsx ==========
function Header() {
return (
<header>
{/* user props를 받지 않음! */}
<UserProfile />
</header>
);
}
// ========== UserProfile.jsx ==========
function UserProfile() {
// Context에서 직접 가져옴!
const user = useUser();
return (
<div className="profile">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// ========== MainContent.jsx ==========
function MainContent() {
return (
<main>
{/* user props를 받지 않음! */}
<ProductSection />
</main>
);
}
// ========== ProductSection.jsx ==========
function ProductSection() {
return (
<div className="products">
{/* user props를 받지 않음! */}
<ProductCard />
</div>
);
}
// ========== ProductCard.jsx ==========
function ProductCard() {
return (
<div className="product">
<h2>맥북 프로</h2>
<p>가격: 2,500,000원</p>
{/* user props를 받지 않음! */}
<BuyButton />
</div>
);
}
// ========== BuyButton.jsx ==========
function BuyButton() {
// Context에서 직접 가져옴!
const user = useUser();
const handleBuy = () => {
alert(`${user.name}님이 구매했습니다!`);
};
return (
<button onClick={handleBuy}>
구매하기
</button>
);
}
React Context API의 장점
Header, MainContent, ProductSection은 user를 받지 않음
필요한 UserProfile과 BuyButton만 Context에서 가져옴
컴포넌트를 수정할 필요 없음
훨씬 깔끔한 구조
Vue
Vue의 기본 원칙
Vue는 v-model을 통해 양방향 바인딩을 지원한다.
Parent Component (상태)
↕️ (양방향 자동 연결)
Child Component (상태 사용)
입력 시: Child → Parent 자동 전달
변경 시: Parent → Child 자동 반영
이전의 단방향 처럼 좋아요 기능을 양방향 상태관리로 구현해보자
<!-- ========== PARENT (app.vue) ========== -->
<template>
<div>
<!-- 1단계: 자식 컴포넌트에 상태를 v-model로 전달 -->
<LikeButton v-model="likes" />
</div>
</template>
<script>
import { ref } from 'vue';
import LikeButton from './LikeButton.vue';
export default {
components: { LikeButton },
setup() {
// 상태 선언
const likes = ref(0);
return { likes };
}
};
</script>
<!-- ========== CHILD (LikeButton.vue) ========== -->
<template>
<div>
<p>❤️ {{ modelValue }}</p>
<!-- 2단계: 버튼 클릭 시 부모에 자동으로 값 전달 -->
<button @click="$emit('update:modelValue', modelValue + 1)">
좋아요
</button>
</div>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
};
</script>
User Click Button
↓
@click 핸들러 실행
↓
$emit('update:modelValue', modelValue + 1) 발생
↓
부모의 v-model이 자동으로 감지
↓
likes 값이 자동으로 업데이트 (0 → 1)
↓
부모의 {{ likes }}가 자동으로 업데이트
↓
자식에게 새로운 modelValue가 props로 전달
↓
자식의 {{ modelValue }}도 자동으로 업데이트
↓
화면에 "❤️ 1" 표시
특징:
- v-model이 자동으로 양방향 연결
- 입력/변경이 자동으로 감지
- props와 emit을 자동으로 처리
이전에 단방향 상태관리의 문제점이었던 깊은 계층에서 상태가 필요한 경우를 양방향 상태관리로 어떻게 해결할수 있는지
예시를 통해 확인해보자
<!-- ========== 최상위 (app.vue) ========== -->
<template>
<Level1 />
</template>
<script>
import { ref, provide } from 'vue';
import Level1 from './Level1.vue';
export default {
components: { Level1 },
setup() {
const user = ref({ name: "Kim" });
// ⭐ user를 모든 자식 컴포넌트에 제공
// (중간 컴포넌트를 거칠 필요 없음!)
provide('user', user);
return { user };
}
};
</script>
<!-- ========== Level1 (아무것도 안 함) ========== -->
<template>
<Level2 />
</template>
<script>
import Level2 from './Level2.vue';
export default {
components: { Level2 }
};
</script>
<!-- ========== Level2 (아무것도 안 함) ========== -->
<template>
<Level3 />
</template>
<script>
import Level3 from './Level3.vue';
export default {
components: { Level3 }
};
</script>
<!-- ========== Level3 (아무것도 안 함) ========== -->
<template>
<Level4 />
</template>
<script>
import Level4 from './Level4.vue';
export default {
components: { Level4 }
};
</script>
<!-- ========== Level4 (직접 주입받음!) ========== -->
<template>
<div>
<p>{{ user.name }}</p>
<button @click="changeUser">이름 변경</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
// Level1, 2, 3을 거칠 필요 없이 직접 주입받음!
const user = inject('user');
const changeUser = () => {
user.value.name = "Lee";
};
return { user, changeUser };
}
};
</script>
양방향의 장점
1. 간단함 - v-model로 자동 동기화
2. Props Drilling 없음 - Provide/Inject로 깊은 계층 접근 가능
3. 빠른 개발 - 보일러플레이트 최소
양방향의 문제점
1. 추적 어려움 - 데이터가 어디서 변경되었는지 찾기 어려울 수 있음
2. 대규모 프로젝트에 부적합 - 여러 곳에서 동시에 변경하면 복잡
React vs Vue 비교
| 항목 | React | Vue |
| 데이터 흐름 | 부모 → 자식만 (명시적) | 양방향 (자동) |
| 입력 처리 | onChange에서 setState | v-model이 자동 처리 |
| 상태 변경 | 부모에서만 가능 | 자식에서도 가능 (emit) |
| Props Drilling | 문제 있음 | Provide/Inject로 해결 |
| 코드량 | 많음 | 적음 |
| 추적 가능성 | 쉬움 | 어려울 수 있음 |
| 명시성 | 높음 | 낮음 (자동 처리) |
결론
단방향 (React 기본): 명확하고 추적 가능하지만, 코드가 많음
양방향 (Vue 기본): 간단하고 빠르지만, 복잡할 수 있음
React도 라이브러리로 양방향 가능
Vue도 엄격한 패턴으로 단방향 가능
→ 프로젝트 특성에 맞게 선택하자!
'공부 > 기술 개념 정리' 카테고리의 다른 글
| React 와 Vue 비교하기 (1) | 2026.01.29 |
|---|---|
| 관심사 분리란? (SoC, separation of concerns) (0) | 2025.12.22 |
| OAuth 프로토콜 (7) | 2025.06.27 |
| Yarn Berry란? (1) | 2025.06.27 |
| 패키지 매니저의 정의와 차이 (yarn, pnpm, npm) (0) | 2025.06.27 |