소개
Redux는 자바스크립트에서 예측 가능한 상태 컨테이너다.
Redux가 처음이라면 여기를 읽고오자.
이 글에서 React Native Application에서 어떻게
Redux
를 이용해 유저 데이터를 유지하는 지 배울 거다.실습 앱은 가상의 소셜 네트워크 앱이다.
HomeScreen
에서 몇명의 친구와 연결됐는지 보여준다.
FriendsScreen
에선 추가 가능한 잠재적 친구들의 리스트를 보여준다.
- 두 스크린 간의 상태를 공유하기 위해
Redux
를 쓸 거다.
셋팅
클론 프로젝트 셋팅 (redux가 반영되지 않은 상태로 클론됨.)
#깃 클론 $ git clone https://github.com/do-community/MySocialNetwork.git #디렉토리 이동 $ cd MySocialNetwork #브랜치 변경 $ git checkout redux-starter #디펜던시들 설치 $ npm install #redux, react-redux 설치 $ npm install redux@4.0.5 react-redux@7.2.1 #ios는 pod 설치까지 $ cd ios $ pod install $ cd ../
Reducer 생성
Redux와 앱을 연결하려면,
reducer
과 action
을 만들어야 함.먼저, friends reducer를 생성하자.
reducer는
이전 상태
와 액션
을 인자로 받고 새로운 상태
를 반환하는 순수함수
(pure function)다.reducer는 앱이 바뀔 때 앱 전체에서 친구들의 현재 상태를 업데이트하는 데 중요하다.
프로젝트 루트 경로에
FriendsReducer.js
파일 생성import { combineReducers } from 'redux'; const INITIAL_STATE = { current: [], possible: [ 'Alice', 'Bob', 'Sammy', ], }; const friendsReducer = (state = INITIAL_STATE, action) => { switch (action.type) { default: return state } }; export default combineReducers({ friends: friendsReducer });
state 초기값은 undefined이므로 초기값을 설정한다. (
INITIAL_STATE
선언)그리고 작성한
reducer
를 export 한다.Action 생성
액션
은 앱에서 Redux store
로 데이터를 보내는 정보의 페이로드를 나타내는 자바스크립트 객체다.type
은 필수로 필요하고, 추가로 페이로드를 전달할지 말진 선택적이다.FriendsActions.js
export const addFriend = friendsIndex => ( { type: 'ADD_FRIEND', payload: friendsIndex, } );
- 액션의
type
을ADD_FRIEND
라고 정하고,
- 추가 페이로드로는 선택된 친구 번호를 담을
friendsIndex
를 선언하였다.
FriendsReducer.js 수정
Action을 추가했으니까, 다시 Reducer를 편집 해보자.
FriendsReducer.js
// ... const friendsReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case 'ADD_FRIEND': //이전 상태(state)에서 current와 possible 값을 가져와서 임시로 담음. //다른 action 요청으로 그동안 값이 바뀔 수 있으므로, state를 직접 바꾸지 않기 위함임 const { current, possible, } = state; //선택된 친구의 번호(friendIndex)가 action.payload에 담겨서 온다. //splice는 배열의 원하는 위치에 요소를 추가,삭제 할 때 쓰이는 함수. (여기선 삭제) //possible 배열에서 action.payload에 있는 요소 1개를 삭제하고, 삭제된 놈이 반환됨. //http://www.gisdeveloper.co.kr/?p=2113 참고 const addedFriend = possible.splice(action.payload, 1); //현재 친구목록에서 해당 친구를 추가함 current.push(addedFriend); //변경된 state를 반영한다. const newState = { current, possible }; return newState; default: return state } }; // ...
Action과 Reducer를 가지고 있으니, 이제 앱에 적용해 보자.
Reducer를 App에 등록하기
friends
state를 React Redux의 Provider 컴포넌트
를 이용해서 제공해야 함.App.js
import 'react-native-gesture-handler'; import React from 'react'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import { StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import friendsReducer from './FriendsReducer'; import HomeScreen from './HomeScreen'; import FriendsScreen from './FriendsScreen'; // ... const store = createStore(friendsReducer); class App extends React.Component { // ... render() { return ( <Provider store={store}> <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Friends" component={FriendsScreen} /> </Stack.Navigator> </NavigationContainer> </Provider> ) } }
이제
앱
에서 friends
state에 접근 가능하다. 이걸
HomeScreen
과 FriendsScreen
에도 추가해야함.Redux를 Screens에 추가
mapStateToProps 함수
를 통해 스크린들(HomeScreen, FriendsScreen)에서 friends
에 접근 가능하도록 만들 수 있다.이 함수는
state
를 FriendsReducer
에서 두개의 스크린의 props
로 맵핑한다.HomeScreen.js
import React from 'react'; import { connect } from 'react-redux'; import { StyleSheet, Text, View, Button } from 'react-native'; class HomeScreen extends React.Component { render() { return ( <View style={styles.container}> <Text>You have { this.props.friends.current.length } friends.</Text> <Button title="Add some friends" onPress={() => this.props.navigation.navigate('Friends') } /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); const mapStateToProps = (state) => { const { friends } = state return { friends } }; export default connect(mapStateToProps)(HomeScreen);
이 코드는
react-redux
를 추가하고, friends
를 HomeScreen
에서도 이용가능하도록 만들었다.아래 5줄로 인해 현재 친구 상태를 { this.props.friends.current.length } 이렇게 접근할 수 있게 된 것임.
FriendsScreen.js
도 수정하자.
import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { StyleSheet, Text, View, Button } from 'react-native'; import { addFriend } from './FriendsActions'; class FriendsScreen extends React.Component { render() { return ( <View style={styles.container}> <Text>Add friends here!</Text> { this.props.friends.possible.map((friend, index) => ( <Button key={ friend } title={ `Add ${ friend }` } onPress={() => this.props.addFriend(index) } /> )) } <Button title="Back to home" onPress={() => this.props.navigation.navigate('Home') } /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); const mapStateToProps = (state) => { const { friends } = state return { friends } }; const mapDispatchToProps = dispatch => ( bindActionCreators({ addFriend, }, dispatch) ); export default connect(mapStateToProps, mapDispatchToProps)(FriendsScreen);
역시
react-redux
를 추가했고, friends
를 FriendsScreen
에서도 접근 가능하도록 만들었다.이제 친구가 될 수 있는 possible friends는 props.friends.possible로 표시할 수 있다.
마지막으로 버튼을 눌렀을 때 동작할 onPress()도 수정했다.
이제 제대로 동작할테니 npx react-native run-ios 해서 돌려보면 됨.
소스코드 깔끔하게!
Action들을
types.js
에 따로 정의해놓고 불러와서 쓰도록 하자.동일한 action 이름을 쓸 때, 한쪽에서 이름을 다르게 바꿔버리면 반영이 안돼서 에러날 수도 있으니까 통일해서 쓰라는 뜻.
types.js
export const ADD_FRIEND = 'ADD_FRIEND';
FriendsActions.js
랑 FriendsReducer.js
에 아래처럼 해서 사용하자.- FriendsActions.js
import { ADD_FRIEND } from './types'; export const addFriend = friendsIndex => ( { type: ADD_FRIEND, payload: friendsIndex, } );
- FriendsReducer.js
import { combineReducers } from 'redux'; import { ADD_FRIEND } from './types'; // ... const friendsReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case ADD_FRIEND: // ... default: return state; } };
코드구조 정리
리덕스 기본 사용구조 준비
- App.js
import { Provider } from 'react-redux'; import { createStore } from 'redux'; import friendsReducer from './FriendsReducer'; //리듀서 함수 불러옴 const store = createStore(friendsReducer); //리듀서 함수를 스토어로 등록 class App extends React.Component{ render(){ return( <Provider store={store} > 메인 컴포넌트 </Provider> ) } }
- FriendsActions.js
//types.js에는 그냥 export const ADD_FRIEND = 'ADD_FRIEND'; 이거임 import {ADD_FRIEND} from './types'; //액션 생성함수 export const addFriend = friendsIndex => ( //액션 { type: ADD_FRIEND, payload: friendsIndex, } );
- friendsReducer.js
import { combineReducers } from 'redux'; //types.js에는 그냥 export const ADD_FRIEND = 'ADD_FRIEND'; 이거임 import { ADD_FRIEND } from './types'; //초기 state는 undefined이므로 초기 상태값 설정. const INITIAL_STATE = { current: [], possible: [ 'Alice', 'Bob', 'Sammy', ], }; //현 상태와 액션을 받아서 액션에 맞는 상태변화를 수행함. const friendsReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case ADD_FRIEND: //상태값 불변성 유지를 위해 상태값에서 값을 가져와서 담음 //다른 액션에 의해서 도중에 값이 바뀔 수도 있다는 것 방지하기 위함. const { current, possible, } = state; //possible에서 payload번째의 배열요소 삭제하고, 삭제된 요소를 반환함. const addedFriend = possible.splice(action.payload, 1); //possible에서 뺀 요소를 current 배열에 추가함. current.push(addedFriend); //redux state를 갱신해서 새로운 상태값 반영 const newState = { current, possible }; return newState; //나머지 액션에 대해선 암것도 안함. default: return state } }; //export default 이므로 import friendsReducer 하면 컴바인된 리듀서가 반환됨. //외부에서 this.props.friends.~~ 이렇게 접근가능함. export default combineReducers({ friends: friendsReducer //friends라는 이에 friendsReducer 리듀서 함수를 맵핑함. });
각 스크린에서 리덕스 사용하기
- HomeScreen.js
- 클래스 컴포넌트라서
connect
를 쓴 거임. 어쩔 수 없는 상황을 제외하고는 함수형으로 작성하도록 하며useSelector
와useDispatch
를 이용하자.
... import { connect } from 'react-redux'; ... class HomeScreen extends React.Component { render() { return ( <View style={styles.container}> <Text>You have { this.props.friends.current.length } friends.</Text> <Button title="Add some friends" onPress={() => this.props.navigation.navigate('Friends') } /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); //이거 때문에 this.props.friends 이렇게 접근 가능한 것. //this.props.friends.posㅋsible 배열 //this.props.friends.current 배열 const mapStateToProps = (state) => { const { friends } = state return { friends } }; //이거 때문에 redux state를 this.props. 이렇게 접근할 수 있는 것임. export default connect(mapStateToProps)(HomeScreen);
- FriendsScreen.js
import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { StyleSheet, Text, View, Button } from 'react-native'; import { addFriend } from './FriendsActions'; //액션생성함수 class FriendsScreen extends React.Component { render() { return ( <View style={styles.container}> <Text>Add friends here!</Text> { this.props.friends.possible.map((friend, index) => ( <Button key={ friend } title={ `Add ${ friend }` } onPress={() => this.props.addFriend(index) //액션생성함수 (액션이랑 바인딩돼서 동일한 이름으로 사용) } /> )) } <Button title="Back to home" onPress={() => this.props.navigation.navigate('Home') } /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, }); //이거 때문에 this.props.friends 이렇게 접근 가능한 것. //this.props.friends.posㅋsible 배열 //this.props.friends.current 배열 const mapStateToProps = (state) => { const { friends } = state return { friends } }; //액션이랑 액션생성함수랑 바인딩 하는 듯 (이름 똑같이, 함수로!) const mapDispatchToProps = dispatch => ( bindActionCreators({ addFriend, }, dispatch) ); export default connect(mapStateToProps, mapDispatchToProps)(FriendsScreen);