공부/기술 개념 정리

단방향 vs 양방향 상태관리: React와 Vue의 핵심 차이

dev_jiwonpark 2026. 1. 29. 19:20

상태관리는 현대 프론트엔드 개발에서 가장 중요한 개념 중 하나라고 생각한다.

특히 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