책 속 문장으로 나의 하루를 기록하는 일기 앱 — 프론트엔드 모노레포
pnpm --filter web dev # http://localhost:3000
pnpm --filter mobile start # Expo Go 앱으로 확인
pnpm turbo dev # 둘 다 동시 실행pnpm turbo teststore/slices/ 에 새 슬라이스 파일을 추가한다.
// store/slices/exampleSlice.ts
import { StateCreator } from 'zustand';
import type { TBoundStore, TStoreMutators } from '../types';
export interface TExampleSlice {
count: number;
increment: () => void;
}
export const createExampleSlice: StateCreator<TBoundStore, TStoreMutators, [], TExampleSlice> = (set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
});store/types.ts 에 타입 추가:
import type { TExampleSlice } from './slices/exampleSlice';
export type TBoundStore = TDiarySlice & TAuthSlice & TExampleSlice;store/useStore.ts 에 슬라이스 연결:
(...a) => ({
...createDiarySlice(...a),
...createAuthSlice(...a),
...createExampleSlice(...a), // 추가
}),'use client';
import useStore from '@/store/useStore';
export default function ExampleComponent() {
// createSelectors로 생성된 .use.{key}() 셀렉터 사용 — 해당 값 변경 시에만 리렌더
const count = useStore.use.count();
const increment = useStore.use.increment();
return <button onClick={increment}>{count}</button>;
}packages/api/src/queryKey.ts 에 도메인별 키 팩토리를 추가한다.
export const exampleKeys = {
all: ['examples'] as const,
list: () => ['examples', 'list'] as const,
detail: (id: string) => ['examples', id] as const,
};'use client';
import { useQuery } from '@tanstack/react-query';
import { diaryKeys } from '@first-penguin/api';
import { apiClient } from '@first-penguin/api';
import type { Diary } from '@first-penguin/types';
export function useDiaryDetail(id: string) {
return useQuery({
queryKey: diaryKeys.detail(id),
queryFn: () => apiClient.get<Diary>(`/diaries/${id}`),
});
}import { useMutation, useQueryClient } from '@tanstack/react-query';
import { diaryKeys } from '@first-penguin/api';
import { apiClient } from '@first-penguin/api';
import type { CreateDiaryRequest, Diary } from '@first-penguin/types';
export function useCreateDiary() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (body: CreateDiaryRequest) =>
apiClient.post<Diary>('/diaries', body),
onSuccess: () => {
// 성공 시 캘린더 목록 자동 리페치
queryClient.invalidateQueries({ queryKey: diaryKeys.list() });
},
});
}'use client';
export default function DiaryDetailPage({ id }: { id: string }) {
const { data, isLoading, isError } = useDiaryDetail(id);
if (isLoading) return <p>로딩 중...</p>;
if (isError) return <p>오류 발생</p>; // 전역 에러는 QueryProvider가 처리
return <p>{data?.quote.text}</p>;
}meta.skipGlobalError: true 를 설정하면 QueryProvider의 전역 에러 핸들러를 건너뛴다.
useQuery({
queryKey: diaryKeys.detail(id),
queryFn: () => apiClient.get(`/diaries/${id}`),
meta: { skipGlobalError: true }, // 이 쿼리는 에러를 직접 처리
});apps/
web/ Next.js 15 (App Router)
mobile/ Expo (React Native)
packages/
types/ 공유 TypeScript 타입
api/ TanStack Query 키 팩토리 + fetch 클라이언트
utils/ 날짜 포맷, 텍스트, HttpError 유틸
ui/ Web 전용 컴포넌트 (shadcn/ui)