[모바일프로그래밍] Zustand 상태관리 & API 통신 패턴 (React Native)
React-native
Zustand 상태관리 & API 통신 패턴 (React Native)
Zustand 기본 철학
상태를 종류별로 분리해서 관리합니다.
-
전역 상태 → Zustand store
-
서버 상태 → React Query / SWR
-
로컬 UI 상태 → useState / useReducer
보일러플레이트 없음, Context 리렌더링 문제 없음.
스토어 구조
도메인별로 파일 분리합니다. Slice Pattern 권장.
각 스토어는 독립적으로 유지 → 관심사 분리, 테스트 용이.
src/store/
├── index.ts # 통합 export
├── useAuthStore.ts # 인증
├── useUserStore.ts # 유저 프로필
├── useUIStore.ts # 모달, 토스트 등
└── useGachaStore.ts # 도메인별
기본 스토어 작성법
State 타입과 Action 타입을 하나의 type으로 명시합니다.
create<T>()(...) 형태로 타입 추론.
RN에서 persist 사용 시 AsyncStorage 반드시 명시. 기본값 localStorage는 RN 미지원.
export const useAuthStore = create<AuthState>()( persist( (set) => ({ token: null, isLoggedIn: false, setToken: (token) => set({ token, isLoggedIn: true }), logout: () => set({ token: null, isLoggedIn: false }), }), { name: 'auth-storage', storage: createJSONStorage(() => AsyncStorage), } ) )
리렌더링 최적화
스토어 전체 구독 금지. 필요한 값만 selector로 구독.
여러 값 동시 구독 시 useShallow 사용.
// 안티패턴 const store = useAuthStore() // 단일 값 const token = useAuthStore((s) => s.token) // 복수 값 const { token, isLoggedIn } = useAuthStore( useShallow((s) => ({ token: s.token, isLoggedIn: s.isLoggedIn })) )
스토어 외부 접근
비컴포넌트(API 레이어, 유틸 함수)에서 훅 없이 직접 접근 가능.
getState() → 읽기, setState() → 쓰기.
const token = useAuthStore.getState().token useAuthStore.setState({ token: 'new-token' })
React Query와 역할 분리
React Query | Zustand | 역할 |
|---|---|---|
서버 데이터 fetch·cache·sync | 클라이언트 전역 상태 | 예시 |
가챠 결과, 시즌 정보 | 슬롯 상태, 인증, UI |
서버에서 받은 데이터를 그대로 store에 넣지 않기.
서버 상태의 source of truth는 React Query가 담당.
React에서 서버 통신 흐름
React에서 서버랑 통신 흐름은 그냥 이거 하나입니다.
Component → fetch/axios → 서버 → JSON → state → 렌더링
-
GET: 데이터 조회
-
POST: 데이터 생성
-
async/await: 비동기 코드 동기처럼 작성
-
try/catch: 실패 대비
핵심은 비동기 + 상태 관리 + 예외 처리 이 세 개.
fetch + useEffect 패턴
state 3개: data / loading / error.
useEffect에서 API 호출. try/catch/finally 구조.
useEffect(() => { const fetchData = async () => { try { const res = await fetch('...'); const data = await res.json(); setPosts(data); } catch (e) { setError(e.message); } finally { setLoading(false); } }; fetchData(); }, []);
여기서 중요한 건:
-
finally에서 loading false 처리
-
에러는 무조건 state로 관리
async/await vs then
둘은 같은 건데 가독성 차이입니다.
then 체인은 흐름이 끊기고, async/await은 위에서 아래로 읽힙니다.
// .then() 방식 fetch(url) .then(res => res.json()) .then(data => console.log(data)); // async/await 방식 const fetchPost = async () => { const res = await fetch(url); const data = await res.json(); console.log(data); };
⇒ 실무에선 async/await 거의 고정
상태 3개는 필수
API 붙이면 무조건 이 3개 있어야 됩니다.
-
loading → 로딩 UI
-
error → 실패 UI
-
data → 실제 렌더링
이거 없으면 UX 이상해지니 조심.
Zustand로 분리
구조를 이렇게 바꾸는 게 핵심입니다.
Component → Store → API → Store → Component
이렇게 하면:
-
컴포넌트에서 fetch 코드 사라짐
-
재사용 가능
-
캐싱됨 (이미 불러온 데이터 재사용)
-
props drilling 없음
⇒ UI랑 로직 분리 (CDD 관심사의 분리)
const usePostStore = create((set) => ({ posts: [], loading: false, error: null, fetchPosts: async () => { set({ loading: true }); try { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await res.json(); set({ posts: data }); } catch (e) { set({ error: e.message }); } finally { set({ loading: false }); } }, }));
⇒ 요청 시작 전에 loading: true로 바꾸고, 끝나면 finally에서 loading: false로 돌려놓는 패턴을 항상 함께 씀
로딩 / 에러 상태 처리
데이터를 불러오는 동안 사용자에게 로딩 화면을 보여줘야 합니다.
const { posts, loading, error, fetchPosts } = usePostStore(); useEffect(() => { fetchPosts(); }, []); if (loading) return <ActivityIndicator />; if (error) return <Text>에러: {error}</Text>; return ( <FlatList data={posts} renderItem={({ item }) => <Text>{item.title}</Text>} /> );
⇒ loading, error, data 세 가지 상태를 항상 같이 관리하는 것이 기본 패턴
Axios
인스턴스
const api = axios.create({ baseURL: '...', timeout: 5000, });
이거 쓰는 이유:
-
baseURL 반복 제거
-
공통 설정 중앙 관리
Interceptor
핵심 개념: 요청/응답 가로채기.
요청 인터셉터
요청 나가기 전에 실행됩니다.
api.interceptors.request.use((config) => { const token = getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });
하는 일:
-
토큰 자동 주입
-
로깅
-
공통 파라미터 처리
응답 인터셉터
응답 들어온 뒤 실행.
api.interceptors.response.use( (res) => res, (err) => { const status = err.response?.status; if (status === 401) logout(); if (status === 500) alert('서버 오류'); return Promise.reject(err); } );
핵심:
-
에러를 여기서 공통 처리
-
그래도 다시 throw 해야 컴포넌트에서도 잡힘
fetch vs axios
fetch | axios | 특징 |
|---|---|---|
기본 제공, 단순함 | 인터셉터, 인스턴스, 자동 JSON | |
기능 적음 | 기능 많음 |
⇒ 규모 커지면 axios가 편함
axios 보안 이슈
2026년 3월 말에 실제로 사고 있었습니다.
-
axios npm 계정 털림
-
악성 버전 배포됨
-
설치하면 RAT 깔리는 구조 🥵🥵🥵🥵
문제 버전:
-
1.14.1
-
0.30.4
원인:
- postinstall에서 악성 코드 실행
⇒ 교훈:
-
버전 고정 안 하면 위험함
-
npm도 공급망 공격 대상임
동기 vs 비동기
동기(Synchronous)
코드가 순서대로 하나씩 실행. 앞 작업이 끝나야 다음 작업 시작.
비동기(Asynchronous)
기다리는 동안 다른 작업 먼저 처리. 서버에서 데이터를 받아오는 작업처럼 시간이 걸리는 일에 사용.
⇒ API 요청은 시간이 걸리기 때문에 비동기로 처리해야 함
fetch() 기본 사용법
fetch()는 서버에 HTTP 요청을 보내는 브라우저 내장 함수.
fetch('https://jsonplaceholder.typicode.com/posts/1') .then(res => res.json()) .then(data => console.log(data));
⇒ .then()을 계속 이어 붙이는 방식인데, 요청이 많아질수록 코드가 복잡해짐
에러 처리 - try/catch
서버 요청은 실패할 수 있기 때문에 try/catch로 에러를 처리.
const fetchPost = async () => { try { const res = await fetch('https://jsonplaceholder.typicode.com/posts/1'); const data = await res.json(); console.log(data); } catch (e) { console.error('요청 실패:', e.message); } finally { console.log('요청 완료'); } };
-
try: 정상적으로 실행할 코드
-
catch: 오류가 생겼을 때 실행할 코드
-
finally: 성공/실패 여부와 관계없이 항상 실행되는 코드
자주 하는 실수
await를 빠뜨리는 경우
// 잘못된 코드 const res = fetch(url); // Promise 객체가 그대로 반환됨 const data = res.json(); // 에러 발생 // 올바른 코드 const res = await fetch(url); const data = await res.json();
async 없이 await를 쓰는 경우
// 잘못된 코드 const fetchData = () => { const res = await fetch(url); // SyntaxError 발생 }; // 올바른 코드 const fetchData = async () => { const res = await fetch(url); };
⇒ await는 반드시 async 함수 안에서만 사용
정리
Zustand는 상태 종류별로 분리해서 관리합니다. 전역 상태는 store, 서버 상태는 React Query, 로컬 UI 상태는 useState. 스토어는 도메인별로 파일 분리하고, 전체 구독 금지. 필요한 값만 selector로 구독합니다.
React에서 서버 통신은 Component → Store → API → Store → Component 흐름. fetch + useEffect 패턴에서 loading, error, data 세 가지 상태를 항상 같이 관리합니다. async/await + try/catch 조합을 쓰면 가독성과 에러 처리 모두 잡을 수 있습니다.
axios는 인터셉터, 인스턴스 등 기능이 많아서 규모가 커지면 편합니다. 하지만 npm 공급망 공격 사고가 있었으니 버전 고정하는 게 안전합니다.
fetch()는 서버에 데이터를 요청하는 함수고, async/await는 그 요청을 동기 코드처럼 읽기 쉽게 써주는 문법입니다. await는 async 함수 안에서만 쓸 수 있고, 빠뜨리면 에러 발생합니다.
상태관리와 API 통신은 서로 다른 역할입니다. 서버 상태는 React Query, 클라이언트 상태는 Zustand. 이 분리를 명확히 하는 게 핵심입니다.
Tags
저자 소개
DevOps Engineer. 금융과 여행에 관심이 많습니다.