[React] React Native와 React의 코드 공유 전략

React와 React Native의 등장으로 웹과 모바일 앱 개발에서 JavaScript를 사용한 크로스 플랫폼 개발이 가능해졌습니다. 이 두 기술은 같은 철학과 비슷한 API를 공유하고 있어, 많은 부분에서 코드를 재사용할 수 있습니다. 이 블로그에서는 React와 React Native 사이의 효과적인 코드 공유 전략에 대해 알아보겠습니다.
1. 프로젝트 구조 설정
효과적인 코드 공유를 위해서는 적절한 프로젝트 구조가 필요합니다. 다음과 같은 구조를 고려해 볼 수 있습니다:
my-project/
├── src/
│   ├── common/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── utils/
│   ├── web/
│   │   ├── components/
│   │   └── pages/
│   └── native/
│       ├── components/
│       └── screens/
├── package.json
└── README.md이 구조에서 common 디렉터리는 공유 코드를 포함하고, web과 native 디렉토리는 각각 플랫폼 특정 코드를 포함합니다.
2. 비즈니스 로직 공유하기
비즈니스 로직은 대부분 플랫폼에 독립적이므로 쉽게 공유할 수 있습니다.
예를 들어, 사용자 인증 로직을 살펴보겠습니다:
// src/common/utils/auth.js
export const validateEmail = (email) => {
  const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return re.test(String(email).toLowerCase());
};
export const authenticateUser = async (email, password) => {
  if (!validateEmail(email)) {
    throw new Error('Invalid email format');
  }
  // 여기에 실제 인증 로직 구현 (예: API 호출)
  const response = await fetch('https://api.example.com/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
    headers: { 'Content-Type': 'application/json' },
  });
  if (!response.ok) {
    throw new Error('Authentication failed');
  }
  return response.json();
};이 코드는 웹과 모바일 앱 모두에서 그대로 사용할 수 있습니다.
3. UI 컴포넌트 추상화
UI 컴포넌트는 플랫폼별로 다르게 구현해야 하지만, 인터페이스를 통일하여 사용 방법을 동일하게 만들 수 있습니다.
예를 들어, 버튼 컴포넌트를 살펴보겠습니다:
// src/common/components/Button.js
import React from 'react';
const Button = ({ onPress, title }) => {
  if (Platform.OS === 'web') {
    return <WebButton onClick={onPress}>{title}</WebButton>;
  } else {
    return <NativeButton onPress={onPress} title={title} />;
  }
};
export default Button;
// src/web/components/WebButton.js
import React from 'react';
const WebButton = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);
export default WebButton;
// src/native/components/NativeButton.js
import React from 'react';
import { Button } from 'react-native';
const NativeButton = ({ onPress, title }) => (
  <Button onPress={onPress} title={title} />
);
export default NativeButton;이렇게 하면 공통 인터페이스를 통해 플랫폼별 구현을 추상화할 수 있습니다.
4. 스타일 공유하기
스타일을 공유하기 위해 플랫폼 독립적인 스타일 객체를 만들 수 있습니다:
// src/common/styles/common.js
export const colors = {
  primary: '#007bff',
  secondary: '#6c757d',
  success: '#28a745',
  danger: '#dc3545',
};
export const fontSizes = {
  small: 12,
  medium: 16,
  large: 20,
};
// 웹용 스타일
export const webStyles = {
  button: {
    backgroundColor: colors.primary,
    color: 'white',
    padding: '10px 20px',
    fontSize: `${fontSizes.medium}px`,
    border: 'none',
    borderRadius: '4px',
  },
};
// React Native용 스타일
export const nativeStyles = {
  button: {
    backgroundColor: colors.primary,
    color: 'white',
    padding: 10,
    fontSize: fontSizes.medium,
    borderRadius: 4,
  },
};이렇게 정의한 스타일은 각 플랫폼의 컴포넌트에서 쉽게 사용할 수 있습니다.
5. 환경 설정 공유하기
환경 설정도 공유할 수 있습니다. 예를 들어, API 엔드포인트나 앱 설정을 공유 파일에 정의할 수 있습니다:
// src/common/config.js
export const API_BASE_URL = 'https://api.example.com';
export const APP_VERSION = '1.0.0';
export const MAX_ITEMS_PER_PAGE = 20;이 설정 파일은 웹과 모바일 앱 모두에서 import 하여 사용할 수 있습니다.
6. 상태 관리 로직 공유하기
Redux나 MobX와 같은 상태 관리 라이브러리를 사용한다면, 상태 관리 로직의 대부분을 공유할 수 있습니다.
예를 들어, Redux 액션과 리듀서를 살펴보겠습니다:
// src/common/redux/userSlice.js
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
  name: 'user',
  initialState: {
    currentUser: null,
    isLoading: false,
    error: null,
  },
  reducers: {
    loginStart: (state) => {
      state.isLoading = true;
    },
    loginSuccess: (state, action) => {
      state.isLoading = false;
      state.currentUser = action.payload;
      state.error = null;
    },
    loginFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    logout: (state) => {
      state.currentUser = null;
    },
  },
});
export const { loginStart, loginSuccess, loginFailure, logout } = userSlice.actions;
export default userSlice.reducer;이 Redux 슬라이스는 웹과 모바일 앱 모두에서 그대로 사용할 수 있습니다.
7. 테스트 코드 공유하기
비즈니스 로직에 대한 테스트 코드도 공유할 수 있습니다. Jest를 사용한 테스트 예제를 살펴보겠습니다:
// src/common/utils/__tests__/auth.test.js
import { validateEmail, authenticateUser } from '../auth';
describe('Auth Utils', () => {
  test('validateEmail returns true for valid email', () => {
    expect(validateEmail('test@example.com')).toBe(true);
  });
  test('validateEmail returns false for invalid email', () => {
    expect(validateEmail('invalid-email')).toBe(false);
  });
  // authenticateUser에 대한 테스트는 모킹이 필요할 수 있습니다
});이 테스트 코드는 웹과 모바일 앱 프로젝트 모두에서 실행할 수 있습니다.
8. 플랫폼별 차이 처리하기
때로는 플랫폼별로 다른 동작이 필요할 수 있습니다. 이런 경우 조건부 로직을 사용할 수 있습니다:
// src/common/utils/platform.js
import { Platform } from 'react-native';
export const isWeb = Platform.OS === 'web';
export const isIOS = Platform.OS === 'ios';
export const isAndroid = Platform.OS === 'android';
export const getPlatformSpecificValue = (webValue, mobileValue) => {
  return isWeb ? webValue : mobileValue;
};이 유틸리티 함수들을 사용하여 플랫폼별 차이를 처리할 수 있습니다.
React와 React Native 사이의 코드 공유는 개발 효율성을 크게 높일 수 있는 전략입니다. 비즈니스 로직, UI 컴포넌트 인터페이스, 스타일, 설정, 상태 관리 로직, 그리고 테스트 코드 등 다양한 영역에서 코드를 공유할 수 있습니다.
효과적인 코드 공유를 위해서는 다음 사항들을 고려해야 합니다:
- 적절한 프로젝트 구조 설계
- 플랫폼 독립적인 로직과 플랫폼 종속적인 구현의 명확한 분리
- 공통 인터페이스를 통한 UI 컴포넌트 추상화
- 플랫폼별 차이를 처리하기 위한 유틸리티 함수 사용
이러한 전략을 적용하면 개발 시간을 단축하고, 코드의 일관성을 유지하며, 버그 발생 가능성을 줄일 수 있습니다. 또한, 새로운 기능을 추가하거나 버그를 수정할 때 한 곳에서 변경사항을 관리할 수 있어 유지보수성도 향상됩니다.
React와 React Native를 함께 사용하는 프로젝트에서는 이러한 코드 공유 전략을 적극적으로 고려해 보시기 바랍니다. 초기에는 약간의 추가 설정과 구조화 작업이 필요할 수 있지만, 장기적으로 볼 때 그 이상의 가치를 제공할 것입니다.