외로운 개인 프로젝트...
리액트 네이티브로 마켓컬리를 똑같이 만들 수 있냐고?
충분해... 하고 싶은거 다 할 수 있어!
: 심플하고 깔끔한 UI를 가졌고 이커머스의 가장 기본적이고 중요한 제품 구매와 관련된 기능에 충실한 앱이다. 때문에 불필요한 기능이나 지저분한 뷰가 없고 Scene들이 군더더기 없이 깔끔하고 아름답다.
(마켓 컬리를 고른것은 아주 완벽한 선택이다)
이커머스 앱을 클론하여 실제 서비스에 대한 이해도를 높이고 클론 중 발생하는 이슈들에 대해 스스로 학습한다.
1) 기능 연습이 아닌 실제 서비스 구현이라는 생각으로 완성도 100%를 실현해본다.
2) 리액트 네이티브에 존재하는 다양한 라이브러리를 최대한 많이 사용해본다.
3) 재활용 가능한 컴포넌트에 집중하여 하드 코딩 일절 없이 개발한다.
4) 마켓컬리에는 없는 메거진 형태의 페이지도 1개 만들어 적용해 본다.(최근 트렌드 반영)
2020년 12월 24일 - 2021년 1월 8일, 16일간
1인 / Xedni(Front-End)
: 카테고리를 제외한 전체 페이지와 주요 기능 전체 + 추가 페이지(메거진 페이지)
<클릭해서 영상으로 이동>
: 앱은 스택이 많기 때문에 처음부터 구조화를 잘 하는 것이 중요하다.
기본적인 5개의 하단 Tab, 그 중 홈 메뉴 안에 또 5개의 Tab, 스택 중 Detail 스택 페이지에 또 5개의 Tab, 그리고 장바구니, 글쓰기, 후기 리스트 등등 수 많은 스택 페이지들...
이 많은 탭과 스택들 어떻게 네스팅 하는지가 관건이었다.
(사실 처음에 Bottom을 부모로 만들었다가 열흘 이상의 시간을 소모했다. 덕분에 큰 깨닳음을 얻었다)
클론이므로 크게 고민하지 않고 당장 보여지는 구조대로 만들어도 상관은 없지만 실제 서비스라면 언제 어떤 스택이 새로 만들어질지 모르기 때문에 Stack Navigation을 가장 최상단에 두기로 했다.
: Stack Navigation이다. 최상단에 위치해 있으며 상품 디테일, 후기작성, 후기리스트, 문의작성, 문의리스트 등등 모든 Stack Screen
들이 위치해 있다.
// App.js
import StackNavigator from './navigation/StackNavigator';
export default App = () => {
return (
<NavigationContainer>
<StatusBar barStyle='light-content' />
<Provider store={store}>
<StackNavigator /> // StackNavigation
</Provider>
</NavigationContainer>
);
};
StackNavigator
만이 존재하는 것을 알 수 있다.export default function StackNavigator() {
// 중간 생략 StackNavigator 렌더 부분
return (
<Stack.Navigator
screenOptions={{/*생략*/}}>
<Stack.Screen
name='main'
component={BottomTabNavigation}
options={/* 장바구니와 컨테이더 내용, 생략*/}/>
<Stack.Screen
name='productDetail'
component={ProductDetail}
options={{
title: '제품상세',
headerShown: false,
}}
/* 아래로 쭉 스택 스크린들이 나열된다... 이하 생략 ... */
/>
</Stack.Navigator>
);
StackNavigator.js
에는 이런식으로 스택으로 쌓을 스크린
들이 아주 많이 모여있다홈 스택
이 이제 기본적으로 보여줄 페이지들의 묶음이다. 이 안에 BottomTab
이 들어있다.react-navigation
의 기본적인 헤더를 사용했다. 거의 마켓컬리와 동일하다.headerShown: false
을 사용해서 일단 헤더를 없애고 새로 만든다.: 홈 스택의 컴포넌트 또한 네비게이션으로 바로 이 BottomTabNavigation
이다.
이 네비게이션은 탭을 만들어주는 네비게이션으로 홈, 추천, 카테고리, 검색, 마이메뉴 를 만들어줄 것이다.
export default function BottomTabNavigation() {
// 중간 생략 BottomTabNavigation 렌더 부분
return (
<Tab.Navigator
initialRouteName={'홈'}
tabBarOptions={/*생략*/}>
<Tab.Screen
name='홈'
component={HomeTabNavigator}
options={/*생략*/}
/>
// ... 아래로 쭉 탭 스크린들이 나열된다... 생략 ...
</Tab.Navigator>
);
}
: 홈 탭의 컴포넌트 또한 또 한번 더 네비게이션이다. 바로 이 HomeTabNavigator
이다.
export default function HomeTabNavigator() {
// 중간 생략 HomeTabNavigator 렌더 부분
return (
<>
<MainTabStack.Navigator
tabBarOptions={{
activeTintColor: Theme.colors.mainColor,
inactiveTintColor: Theme.colors.grayColor,
pressColor: Theme.colors.mainColor,
indicatorStyle: {
borderBottomWidth: 3,
borderBottomColor: Theme.colors.mainColor,
},
labelStyle: {
fontSize: 13,
fontWeight: '600',
},
}}>
<MainTabStack.Screen name='컬리추천' component={HomeRecommend} />
<MainTabStack.Screen name='신상품' component={NewProduct} />
<MainTabStack.Screen name='베스트' component={BestProduct} />
<MainTabStack.Screen name='알뜰쇼핑' component={Shopping} />
<MainTabStack.Screen name='이벤트' component={Event} />
</MainTabStack.Navigator>
</>
);
}
: 토글이나 오프셋 등의 State는 씬에서 개별적으로 사용되며 제품 정보와 같은 스택에서 사용되는 상태는 리덕스에서 관리된다.
// ProductDetail Redux 부분
export default setProductData = (state = productDetailData, action) => {
if (action.type === 'setDetailData') {
let copyState = { ...state };
copyState = action.payload;
return copyState;
} else if (action.type === 'addReview') {
let copyState = { ...state };
let reviewState = [...state.review].concat(action.payload);
copyState.review = reviewState;
return copyState;
}
};
payload
로 넘겨 어떤 곳에서 스택을 띄워도 해당 제품에 맞는 디테일 씬이 뜨도록 만들어 두었다. <FlatContainer
onPress={() => {
goToDetail();
props.dispatch({
type: 'setDetailData',
payload: item,
});
}}
>
payload
를 보내줄때는 dispatch
를 사용하여 보내준다.const store = createStore(setProductData);
export default App = () => {
return (
<NavigationContainer>
<StatusBar barStyle='light-content' />
<Provider store={store}>
<StackNavigator />
</Provider>
</NavigationContainer>
);
};
function setRedux(state) {
return {
state: state,
};
}
export default connect(setRedux)(/*blabla*/);
: 가장 메인이 되는 화면이기 때문에 재밌는 기능이 많다.
: 홍보용 배너 컴포넌트가 1개, 상품리스트 컴포넌트가 4개 있다.
// 맨 위로 올리기 기능 렌더 부분
onScrollEndDrag={showButton} // 스크롤뷰 속성
const showButton = (e) => {
if (e.nativeEvent.contentOffset.y - 0 > 400) {
setToggle(true);
} else {
setToggle(false);
}
};
const onUpPage = () => {
scrollLocation.current.scrollTo({ y: 0 });
};
//렌더 부분
<FlatList
data={props.data}
onEndReached={props.onEndReached}
keyExtractor={(product) => String(product.id)}
onEndReachedThreshold={0.8}
horizontal={true}
showsHorizontalScrollIndicator={false}
renderItem={renderItem}
ListFooterComponent={
props.loading ? <ActivityIndicator size='large' /> : null
}
/>
onEndReachedThreshold
속성으로 무한 스크롤 기능을 만들 수 있는데 스크롤이 0.8로 설정해두어 80% 정도 내려갔을때 새로운 데이터를 불러온다. const getProductData = async () => {
if (loading) {
return;
}
setLoading(true);
await fetch(
'ProductAPI',
{ method: 'GET' }
)
.then((res) => res.json())
.then((result) => {
setProductData([
...productData,
...result.slice(offset, offset + LIMIT),
]);
setOffset(offset + LIMIT);
});
setLoading(false);
};
offset
, LIMIT
으로 LIMIT 값을 설정해 두었기 때문에const LIMIT = 4;
: HomeRecommend의 또 다른 컴포넌트로 레시피 공개하는 상품들의 리스트롤 보여준다. 위의 HomeProductFlatList와 거의 비슷하니 생략하고 타이머 기능면 소개해보자면...
import moment from 'moment';
const [days, setDays] = useState(0);
const [hours, setHours] = useState(0);
const [mins, setMins] = useState(0);
const [secs, setSecs] = useState(0);
const [eventDate, setEventDate] = useState(
moment.duration().add({ days: 3, hours: 13, minutes: 30, seconds: 60 })
);
// useEffect 부분
useEffect(() => {
updateTimer();
}, []);
// 타이머 함수
const updateTimer = () => {
const x = setInterval(() => {
if (eventDate <= 0) {
clearInterval(x);
} else {
setEventDate(eventDate.subtract(1, 's'));
setDays(eventDate.days());
setHours(eventDate.hours());
setMins(eventDate.minutes());
setSecs(eventDate.seconds());
}
}, 1000);
};
useEffect
를 통해 페이지 시작과 동시에 설정해둔 시간이 흐른다. (초기값을 new Data()
로 현재 시간을 기준으로 한다면 D-day기능이 되겠지?): 신상품, 베스트, 알뜰쇼핑은 전부 동일한 FlatList 컴포넌트를 재활용해서 API통신만 달리하고 있다.
(그러니 나머지는 생략)
import RNPickerSelect from 'react-native-picker-select';
react-native-pickerSelect
라이브러리를 사용했다.const [sort, setSort] = useState('new');
// 렌더부분
<RNPickerSelect
style={/*생략*/}
placeholder={{
label: '정렬필터',
value: null,
}}
items={ITEM}
onValueChange={(value) => {
handleSelect(value);
setSort(value);
}}
value={sort}
/>
RNPickerSelect
는 초기 정렬값을 state로 관리할 수 있다.const ITEM = [
{
label: '신상품순',
value: 'new',
},
{
label: '낮은가격순',
value: 'low',
},
{
label: '높은가격순',
value: 'high',
},
{
label: '추천순',
value: 'recommend',
},
];
RNPickerSelect
의 items
에 적용된다.스택 스크린들과 Bottom 메뉴들이 궁금하다면 2화에서 계속
진짜 엄청 잘하셨어요!! 항상 배우고 갑니다 ㅎㅎ