Expo와 Next.js 모노레포 구축 가이드

웹과 모바일 앱을 위한 모노레포 아키텍처 구현 방법


Expo와 Next.js 모노레포 구축 가이드

Expo와 Next.js를 활용한 모노레포 아키텍처는 웹과 모바일 앱 개발을 단일 코드베이스에서 효율적으로 관리할 수 있는 강력한 방법. 이 글에서는 모노레포 설정부터 공통 컴포넌트 구현까지 전체 프로세스를 단계별로 살펴볼 것.

모노레포 도입의 이점

모노레포(Monorepo)는 여러 프로젝트를 하나의 저장소에서 관리하는 개발 방식으로, 다음과 같은 장점이 있음:

  • 코드 재사용: 동일한 UI 컴포넌트와 비즈니스 로직을 웹과 앱에서 공유
  • 일관성 유지: 모든 플랫폼에서 동일한 디자인 시스템 적용
  • 개발 효율성: 한 번의 변경으로 모든 플랫폼에 적용 가능
  • 의존성 관리 간소화: 패키지 버전 충돌 최소화

프로젝트 구조 설계

효율적인 모노레포를 위한 폴더 구조는 다음과 같음:

expo-next-monorepo/
├─ apps/
│  ├─ native/ (Expo 앱)
│  └─ web/ (Next.js 앱)
├─ packages/
│  ├─ ui/ (공통 UI 컴포넌트)
│  └─ app/ (공통 로직)
├─ scripts/
├─ package.json
└─ turbo.json

이 구조에서 각 폴더의 역할은 다음과 같음:

  • apps: 각 플랫폼별 애플리케이션 코드
  • packages: 공통으로 사용되는 코드 모음
  • scripts: 빌드 및 개발 관련 스크립트

이 구조는 코드 공유와 분리의 균형을 맞추어 효율적인 개발 환경을 제공

Yarn Workspaces 설정

모노레포의 핵심 기술은 Yarn Workspaces. 루트 디렉토리의 package.json 설정은 다음과 같음:

{
  "name": "expo-next-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "dev": "turbo run dev",
    "build": "turbo run build",
    "lint": "turbo run lint"
  },
  "resolutions": {
    "react": "19.0.0",
    "react-native": "0.79.6",
    "react-native-web": "~0.20.0",
    "tailwindcss": "^3.4.1"
  }
}

이 설정의 주요 요소:

  • workspaces: Yarn에게 appspackages 폴더의 모든 프로젝트를 하나의 모노레포로 인식하도록 지시
  • resolutions: 패키지 버전 충돌을 방지하기 위한 의존성 버전 고정
  • scripts: 모든 워크스페이스에서 동일한 명령을 실행하기 위한 루트 스크립트

Turborepo 빌드 최적화

Turborepo는 모노레포 빌드 속도를 크게 향상시키는 도구. 다음과 같이 설정:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "dependsOn": ["^build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Turborepo의 주요 기능:

  • 의존성 기반 실행 순서: dependsOn: ["^build"] 설정으로 의존성 있는 패키지가 먼저 빌드됨
  • 캐싱: 이전 빌드 결과를 재사용하여 빌드 시간 단축
  • 병렬 실행: 독립적인 작업을 동시에 실행하여 전체 빌드 시간 최소화

공통 UI 컴포넌트 구현

공통 UI 컴포넌트 개발은 모노레포의 핵심 가치. 아래는 기본 Button 컴포넌트 구현 예시:

// packages/ui/Button/index.tsx
import React from "react";
import { Pressable, Text, StyleSheet, PressableProps } from "react-native";
 
export interface ButtonProps extends PressableProps {
  title: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}
 
export const Button = ({
  title,
  variant = "primary",
  disabled = false,
  ...props
}: ButtonProps) => {
  const buttonStyle = [
    styles.button,
    variant === "primary" ? styles.primaryButton : styles.secondaryButton,
    disabled && styles.disabledButton,
  ];
 
  const textStyle = [
    styles.text,
    variant === "primary" ? styles.primaryText : styles.secondaryText,
  ];
 
  return (
    <Pressable style={buttonStyle} disabled={disabled} {...props}>
      <Text style={textStyle}>{title}</Text>
    </Pressable>
  );
};
 
const styles = StyleSheet.create({
  button: {
    padding: 8,
    borderRadius: 6,
    alignItems: "center",
    justifyContent: "center",
  },
  primaryButton: {
    backgroundColor: "#dc2626",
  },
  secondaryButton: {
    backgroundColor: "#9ca3af",
  },
  disabledButton: {
    opacity: 0.5,
  },
  text: {
    fontWeight: "bold",
  },
  primaryText: {
    color: "#ffffff",
  },
  secondaryText: {
    color: "#111827",
  },
});

UI 컴포넌트 패키지 내보내기는 다음과 같이 설정:

// packages/ui/index.ts
export * from "./Button";
export * from "./Text";
export * from "./View";

각 애플리케이션에서는 다음과 같이 의존성을 설정:

// apps/native/package.json 또는 apps/web/package.json
{
  "dependencies": {
    "ui": "*"
  }
}

여기서 "ui": "*"는 로컬 패키지를 참조하는 Yarn Workspaces 문법으로, 자동으로 링크됨

크로스 플랫폼 컴포넌트 활용

공통 컴포넌트를 네이티브 앱과 웹 앱에서 각각 활용하는 방법을 살펴볼 것:

네이티브 앱 (Expo) 구현

// apps/native/app/index.tsx
import "../global.css";
import { View, Text, Button } from "ui";
import { ScrollView } from "react-native";
 
export default function Index() {
  return (
    <ScrollView>
      <View className="flex-1 items-center p-6">
        <Text variant="h1" color="primary" className="mb-8 mt-12">
          테스트 화면
        </Text>
 
        <Button
          title="네이티브에서 눌러봐"
          onPress={() => alert("버튼 클릭 이벤트")}
        />
 
        <Button
          title="이것도 눌러봐"
          variant="secondary"
          onPress={() => alert("secondary 버튼 클릭")}
        />
      </View>
    </ScrollView>
  );
}

웹 앱 (Next.js) 구현

// apps/web/app/page.tsx
"use client";
import { View, Text, Button } from "ui";
 
export default function Home() {
  return (
    <View className="flex-1 items-center p-6">
      <Text variant="h1" color="primary" className="mb-8">
        테스트 화면
      </Text>
 
      <Button title="웹에서 눌러봐" onPress={() => alert("버튼 클릭 이벤트")} />
 
      <Button
        title="이것도 눌러봐"
        variant="secondary"
        onPress={() => alert("secondary 버튼 클릭")}
      />
    </View>
  );
}

이 구현의 핵심은 React Native 컴포넌트가 React Native Web을 통해 웹에서도 동일하게 작동한다는 점. 한 번 작성한 코드를 여러 플랫폼에서 재사용함으로써 개발 효율성이 크게 향상

모노레포 구현 시 주요 고려사항

모노레포 구현 과정에서 발생할 수 있는 여러 기술적 과제와 해결 방법을 알아볼 것:

1. 의존성 버전 관리

다양한 프로젝트 간 의존성 버전 충돌은 모노레포에서 흔히 발생하는 문제. 특히 React, React Native, React Native Web 버전 간 호환성 문제가 자주 발생.

해결책: 루트 package.json의 resolutions 필드를 활용하여 핵심 의존성의 버전을 고정.

2. 플랫폼별 코드 분리

UI 컴포넌트가 iOS, 안드로이드, 웹에서 각각 다르게 동작해야 하는 경우가 있음. 이런 경우 React Native의 파일 확장자 기반 플랫폼 분리 기능을 활용 가능:

IconSymbol.tsx     # 기본 구현 (모든 플랫폼)
IconSymbol.ios.tsx # iOS용 구현
IconSymbol.web.tsx # 웹용 구현

이 방식을 사용하면 플랫폼별 코드를 깔끔하게 분리하면서도 동일한 API를 유지 가능.

3. 타입스크립트 설정 통합

여러 프로젝트 간 일관된 타입스크립트 설정을 유지하는 것이 중요. 다음과 같이 기본 설정을 공유할 수 있음:

// packages/tsconfig/base.json (공통 설정)
{
  "compilerOptions": {
    "strict": true,
    "jsx": "react-native",
    // ...
  }
}
 
// apps/native/tsconfig.json
{
  "extends": "../../packages/tsconfig/base.json",
  "compilerOptions": {
    // 추가 설정
  }
}

이 접근 방식을 통해 일관된 타입 정의와 컴파일 옵션을 유지하면서도 프로젝트별 특수성을 수용 가능.

확장 및 최적화 전략

모노레포 기반 개발 환경을 더욱 개선하기 위한 주요 전략은 다음과 같음:

1. UI 컴포넌트 라이브러리 확장

기본 컴포넌트 세트를 넘어 더 다양한 UI 컴포넌트로 확장하는 것이 중요:

  • Form 요소 (Input, Select, Checkbox 등)
  • Modal 및 Dialog 컴포넌트
  • 데이터 표시 컴포넌트 (Card, Table 등)
  • 네비게이션 요소

2. 상태 관리 통합

효과적인 상태 관리를 위해 다음과 같은 접근법 고려 가능:

  • Zustand나 Jotai와 같은 경량 상태 관리 라이브러리 도입
  • 공통 상태 로직을 packages/app에 구현
  • 서버 상태와 클라이언트 상태 분리 관리

3. 성능 최적화

다음과 같은 성능 최적화 기법 적용 가능:

  • React Native Web 최적화 (트리 쉐이킹, 코드 스플리팅)
  • 메모이제이션 활용 (React.memo, useMemo)
  • 번들 크기 최적화

4. API 레이어 공통화

효율적인 API 통합을 위한 전략:

  • REST/GraphQL 클라이언트 공통화
  • API 타입 정의 공유
  • 데이터 페칭 로직 추상화

결론

Expo와 Next.js를 활용한 모노레포 아키텍처는 웹과 모바일 앱 개발을 위한 강력한 접근 방식. 초기 설정의 복잡성에도 불구하고, 코드 공유와 일관성 유지, 개발 효율성 측면에서 큰 이점을 제공.

성공적인 모노레포 구현을 위한 핵심 요소:

  • 명확한 프로젝트 구조 설계
  • 효과적인 의존성 관리
  • 크로스 플랫폼 컴포넌트 설계
  • 지속적인 최적화와 확장

모든 프로젝트에 모노레포가 적합한 것은 아니지만, 웹과 모바일 애플리케이션을 동시에 개발하는 팀에게는 강력한 생산성 향상 도구가 될 수 있음.