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를 함께 사용하는 프로젝트에서는 이러한 코드 공유 전략을 적극적으로 고려해 보시기 바랍니다. 초기에는 약간의 추가 설정과 구조화 작업이 필요할 수 있지만, 장기적으로 볼 때 그 이상의 가치를 제공할 것입니다.