PromleeBlog
sitemap
aboutMe

posting thumbnail
React (Native)에서 Context API로 전역변수 사용하기
Using Global Variables with Context API in React (Native)

📅

🚀

들어가기 전에 🔗

React (Native) 앱을 개발하다 보면 여러 컴포넌트 간에 데이터를 전달하거나 공유해야 할 때가 많습니다. 이때 많이 사용하는 기술이 바로 Context API입니다. Context API를 사용하면 여러 단계로 props를 전달하지 않고도 전역 상태를 간편하게 관리할 수 있습니다. 이 글에서는 React Native에서 Context API를 어떻게 사용하는지, 그리고 왜 유용한지 쉽게 이해할 수 있도록 차근차근 설명드리겠습니다.
물론 React 웹 개발 환경에서도 동일하게 적용할 수 있습니다.
다음은 Expo 프로젝트 기준 구성되어있는 Flie 구조입니다. 참고하여 미리 프로젝트 구조를 생성해두시면 좋습니다.
📦 [프로젝트 폴더]
┣ 📂 app
┃ ┗ 📜 index.tsx
┣ 📂 components
┃ ┣ 📜 CounterDisplay.tsx
┃ ┗ 📜 CouterButton.tsx
┣ 📂 context
┗ ┗ 📜 CounterContext.tsx
Tip: 나중에 상태 관리 라이브러리(Redux 등)를 도입하기 전, 간단한 전역 상태 관리는 Context API로 충분히 처리할 수 있습니다.

🚀

Context API란? 🔗

Context API는 React에서 기본적으로 제공하는 기능으로, 컴포넌트 트리 안에서 전역 데이터를 정의하고 쉽게 사용할 수 있도록 도와줍니다. 즉, 한 곳에서 생성한 데이터를 여러 컴포넌트에서 직접 구독하여 사용함으로써, 복잡한 props 전달의 번거로움을 덜어줍니다.

주요 장점 🔗


🚀

Context API 사용법 🔗

Context API를 사용하기 위한 기본 단계는 다음과 같습니다
  1. Context 생성: React.createContext()를 사용하여 Context를 생성합니다.
  2. Provider 설정: 생성한 Context의 Provider 컴포넌트를 통해 전역 상태를 하위 컴포넌트에 전달합니다.
  3. Consumer 사용: 하위 컴포넌트에서는 useContext() Hook(또는 Consumer 컴포넌트)을 사용하여 Context에 담긴 데이터를 사용합니다.

🚀

Context로 카운터 앱 만들기 🔗

1. Context 생성하기 🔗

먼저 카운터 앱을 만들기 위해 Context를 생성합니다.
  1. typescript 를 사용하는 경우, Context에 들어갈 데이터 타입을 지정해줍니다. 여기서는 countsetCount를 포함하는 CounterContextType을 정의합니다.
  2. createContext() 함수를 사용하여 Context를 생성합니다.
  3. useCounter Hook을 만들어 Context를 사용할 수 있도록 합니다. 이 Hook은 Context를 사용할 때마다 Provider로 감싸져 있는지 확인하고, Context의 값을 반환합니다.
  4. CounterProvider 컴포넌트를 만들어 useState를 사용해 countsetCount를 관리하고, 이를 Context의 value로 전달합니다.
/app/context/CounterContext.tsx
import React, { createContext, useState, useContext } from 'react';
 
// 1. Context 생성하기
interface CounterContextType {
	count: number;
	setCount: React.Dispatch<React.SetStateAction<number>>;
}
 
const CounterContext = createContext<CounterContextType | null>(null);
 
export const useCounter = () => {
	const context = useContext(CounterContext);
	if (!context) {
		throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
	}
	return context;
};
 
export const CounterProvider: React.FC = ({ children }) => {
	const [count, setCount] = useState(0);
	const value = { count, setCount };
 
	return (
		<CounterContext.Provider value={value}>
			{children}
		</CounterContext.Provider>
	);
};
 
export default CounterContext;

2. Context 사용하기 🔗

다음으로 카운터를 표시하고 증가시키는 컴포넌트를 만듭니다. 각 컴포넌트에서 useCounter Hook을 사용하여 Context의 값을 가져옵니다. 서로 다른 컴포넌트에서 같은 Context를 사용할 수 있습니다.
/app/components/CounterDisplay.tsx
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useCounter } from '../context/CounterContext';
 
const CounterDisplay = () => {
	const { count } = useCounter();
 
	return (
		<Text style={styles.countText}>현재 카운트: {count}</Text>
	);
};
 
const styles = StyleSheet.create({
	countText: {
		fontSize: 24,
		textAlign: 'center',
		marginBottom: 20,
	},
});
 
export default CounterDisplay;
/app/components/CounterButton.tsx
import React from 'react';
import { Button } from 'react-native';
import { useCounter } from '../context/CounterContext';
 
const CounterButton = () => {
	const { setCount } = useCounter();
 
	return (
		<Button title="카운트 증가" onPress={() => setCount(prev => prev + 1)} />
	);
};
 
export default CounterButton;

3. Provider로 감싸기 🔗

마지막으로 두 컴포넌트를 모두 렌더링하는 index.tsx에서 컴포넌트를 CounterProvider로 감싸고, CounterDisplayCounterButton을 사용합니다.
/app/index.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import CounterDisplay from './components/CounterDisplay';
import CounterButton from './components/CounterButton';
import { CounterProvider } from './context/CounterContext';
 
const App = () => {
	return (
		<CounterProvider>
			<View style={styles.container}>
				<CounterDisplay />
				<CounterButton />
			</View>
		</CounterProvider>
	);
};
 
const styles = StyleSheet.create({
	container: {
		flex: 1,
		justifyContent: 'center',
		paddingHorizontal: 20,
		backgroundColor: '#fff',
	},
});
 
export default App;
한 파일에 합쳐진 전체 코드 보기
/app/index.tsx
import React, { createContext, useState, useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
 
// 1. Context 생성하기
// (타입스크립트를 사용하는 경우, Context에 들어갈 데이터 타입을 지정해줍니다.)
interface CounterContextType {
  count: number;
  setCount: React.Dispatch<React.SetStateAction<number>>;
}
 
const CounterContext = createContext<CounterContextType | null>(null);
 
// 2. Provider 컴포넌트 만들기
const CounterProvider = ({ children }: { children: React.ReactNode }) => {
  const [count, setCount] = useState(0);
  
  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
};
 
// 3. Context를 사용하는 컴포넌트 만들기
const CounterDisplay = () => {
  // useContext를 통해 Context의 값을 가져옵니다.
  const counterContext = useContext(CounterContext);
  if (!counterContext) {
    throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
  }
  const { count } = counterContext;
 
  return (
    <Text style={styles.countText}>현재 카운트: {count}</Text>
  );
};
 
const CounterButton = () => {
  const counterContext = useContext(CounterContext);
  if (!counterContext) {
    throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
  }
  const { setCount } = counterContext;
 
  return (
    <Button title="카운트 증가" onPress={() => setCount(prev => prev + 1)} />
  );
};
 
const App = () => {
  return (
    <CounterProvider>
      <View style={styles.container}>
        <CounterDisplay />
        <CounterButton />
      </View>
    </CounterProvider>
  );
};
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#fff',
  },
  countText: {
    fontSize: 24,
    textAlign: 'center',
    marginBottom: 20,
  },
});
 
export default App;
➡️

실행 결과 🔗

image

🚀

useReducer와 함께 증가/감소 카운터 만들기 🔗

더 복잡한 상태 관리가 필요할 때는 useReducer와 Context를 함께 사용하는 방법도 고려해볼 수 있습니다.
아래 예제는 카운터 상태를 useReducer를 통해 관리하는 방법입니다.

1. useReducer 사용하여 Context 만들기 🔗

  1. 상태 타입과 액션 타입을 정의합니다. 여기서는 각각 count 상태와 INCREMENT, DECREMENT 액션을 정의합니다.
  2. useReducer 함수를 사용하여 상태와 액션을 관리하는 counterReducer 함수를 만듭니다.
  3. CounterProvider 컴포넌트에서 useReducer를 사용하여 상태와 dispatch 함수를 생성하고, Context에 전달합니다.
/app/context/CounterContext.tsx
import React, { createContext, useReducer, useContext } from 'react';
 
// 상태 타입과 액션 타입 정의
interface CounterState {
	count: number;
}
 
type ActionType = { type: 'INCREMENT' } | { type: 'DECREMENT' };
 
const initialState: CounterState = { count: 0 };
 
const counterReducer = (state: CounterState, action: ActionType): CounterState => {
	switch(action.type) {
		case 'INCREMENT':
			return { count: state.count + 1 };
		case 'DECREMENT':
			return { count: state.count - 1 };
		default:
			return state;
	}
};
 
// Context 타입 정의
interface CounterContextType {
	state: CounterState;
	dispatch: React.Dispatch<ActionType>;
}
 
const CounterContext = createContext<CounterContextType | null>(null);
 
// Provider 컴포넌트 생성
export const CounterProvider: React.FC = ({ children }) => {
	const [state, dispatch] = useReducer(counterReducer, initialState);
	
	return (
		<CounterContext.Provider value={{ state, dispatch }}>
			{children}
		</CounterContext.Provider>
	);
};
 
export const useCounter = () => {
	const context = useContext(CounterContext);
	if (!context) {
		throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
	}
	return context;
};
 
export default CounterContext;

2. 상태와 dispatch 사용하기 🔗

CounterDisplayCounterButtons 컴포넌트에서는 useCounter Hook을 사용하여 상태와 dispatch 함수를 가져옵니다.
/app/components/CounterDisplay.tsx
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useCounter } from '../context/CounterContext';
 
const CounterDisplay = () => {
	const { state } = useCounter();
 
	return (
		<Text style={styles.countText}>현재 카운트: {state.count}</Text>
	);
};
 
const styles = StyleSheet.create({
	countText: {
		fontSize: 24,
		textAlign: 'center',
		marginBottom: 20,
	},
});
 
export default CounterDisplay;
/app/components/CounterButtons.tsx
import React from 'react';
import { View, Button } from 'react-native';
import { useCounter } from '../context/CounterContext';
 
const CounterButtons = () => {
	const { dispatch } = useCounter();
 
	return (
		<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
			<Button title="감소" onPress={() => dispatch({ type: 'DECREMENT' })} />
			<Button title="증가" onPress={() => dispatch({ type: 'INCREMENT' })} />
		</View>
	);
};
 
export default CounterButtons;

3. Provider로 감싸기 🔗

마지막으로 CounterProviderCounterDisplayCounterButtons를 감싸고, 렌더링합니다.
/app/index.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import CounterDisplay from './components/CounterDisplay';
import CounterButtons from './components/CounterButtons';
import { CounterProvider } from './context/CounterContext';
 
const App = () => {
	return (
		<CounterProvider>
			<View style={styles.container}>
				<CounterDisplay />
				<CounterButtons />
			</View>
		</CounterProvider>
	);
};
 
const styles = StyleSheet.create({
	container: {
		flex: 1,
		justifyContent: 'center',
		paddingHorizontal: 20,
		backgroundColor: '#fff',
	},
});
 
export default App;
한 파일에 합쳐진 전체 코드 보기
// App.tsx
import React, { createContext, useReducer, useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
 
// 상태 타입과 액션 타입 정의
interface CounterState {
  count: number;
}
 
type ActionType = { type: 'INCREMENT' } | { type: 'DECREMENT' };
 
const initialState: CounterState = { count: 0 };
 
const counterReducer = (state: CounterState, action: ActionType): CounterState => {
  switch(action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};
 
// Context 타입 정의
interface CounterContextType {
  state: CounterState;
  dispatch: React.Dispatch<ActionType>;
}
 
const CounterContext = createContext<CounterContextType | null>(null);
 
// Provider 컴포넌트 생성
const CounterProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(counterReducer, initialState);
  
  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
};
 
const CounterDisplay = () => {
  const counterContext = useContext(CounterContext);
  if (!counterContext) {
    throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
  }
  const { state } = counterContext;
 
  return (
    <Text style={styles.countText}>현재 카운트: {state.count}</Text>
  );
};
 
const CounterButtons = () => {
  const counterContext = useContext(CounterContext);
  if (!counterContext) {
    throw new Error("CounterContext가 Provider로 감싸져 있지 않습니다.");
  }
  const { dispatch } = counterContext;
 
  return (
    <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
      <Button title="감소" onPress={() => dispatch({ type: 'DECREMENT' })} />
      <Button title="증가" onPress={() => dispatch({ type: 'INCREMENT' })} />
    </View>
  );
};
 
const App = () => {
  return (
    <CounterProvider>
      <View style={styles.container}>
        <CounterDisplay />
        <CounterButtons />
      </View>
    </CounterProvider>
  );
};
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 20,
    backgroundColor: '#fff',
  },
  countText: {
    fontSize: 24,
    textAlign: 'center',
    marginBottom: 20,
  },
});
 
export default App;
➡️

실행 결과 🔗

image

🚀

결론 🔗

Context API는 React Native 앱에서 전역 상태를 간편하게 관리할 수 있는 강력한 도구입니다. 간단한 데이터 공유부터 복잡한 상태 관리까지 다양한 시나리오에 활용할 수 있습니다. 초보자라면 위의 기본 예제를 먼저 시도해보고, 필요에 따라 useReducer와 같이 확장해 나가시면 좋습니다.

더 생각해 보기 🔗

참고 자료 🔗