앱을 개발 할 때 가장 중요한 부분중 하나가 일관성 있고 구조가 잘 잡힌 로컬 데이터 핸들링 입니다.

 

리덕스는 앱에 단 하나뿐인 전역 상태 객체로 앱 내 모든 Component에 props로 전달되고 데이터가 수정되면 변경된 데이터가 다시 전달됩니다.

 

리덕스는 store라는 곳에서 데이터를 관리하고 전역 state를 만들고 관리하는 context(react API)라는 기능을 이용합니다. 

 

리덕스 예제 구현

도서 목록을 관리하는 앱을 통해 리덕스를 구현해 보겠습니다. 

 

1. 리덕스 관련 의존성 라이브러리 설치

npm install --save redux react-redux

*설치 이후에 에뮬레이터 캐쉬를 비워줘야 하는건지 빌드, 실행이 잘 되지 않았네요..

 

2. actions.js, Books.js 파일을 생성하고 reducers 폴더와 그 아래 bookReducer.js, index.js를 생성합니다. 

* 리듀서는 객체를 반환하는 함수로 여러 리듀서들을 묶어 전역 state를 만들게 됩니다.

 

3. bookReducer.js에 책 객체를 정의하고 state를 반환하는 함수 생성

4. index.js에 루트 리듀서 생성(전역 state생성)

- redex에서 combineReducers import

- bookReducer를 import하여 combineReducers를 통해 루트 리듀서 생성

 

5. App.js에 리덕스 스토어를 만들고 provider를 통해 자식 Component에서 스토어를 참조할 수 있게 함

- Provider를 react-redux import하여 가져오기

- createStore를 redux import하여 가져오기

- rootReducer(reducers/index.js)를 이용하여 store 객체 생성 

- <Provider store={store}>로 감싼 자식 Component에 store를 전달

 

6. Books.js

* connect 함수를 이용해서 자식 component에서 데이터 참조, 커링함수 : 다른 함수를 반환하는 함수

- connect를 react-redux에서 import

- const {books} = this.props으로 books 데이터를 사용

const mapStateToProps = (state) => ({
  books: state.bookReducer.books
})
export default connect(mapStateToProps)(Books)

books 배열을 리덕스의 전역 state에서 꺼내 객체를 반환하고 connect 함수의 결과를 외부에 export

 

 7. 책을 추가할 수 있도록 actions.js에 액션 추가하기

- export const ADD_BOOK = 'ADD_BOOK' //리듀서에서 재사용할 수 있도록 상수 export

- addBook이라는 함수를 만들고 type과 추가할 book 객체를 반환

 

8. bookReducer에서 addBook 액션을 이용할 수 있게 만듦

- actions에서 ADD_BOOK을 import

- bookReducer의 두번째 인자로 action을 추가

- actions.type으로 분기하여 books state를 업데이트 할 수 있도록 로직 추가

* ...state.books는 스프레드 연산자로 기존 배열에 추가할 action.book을 더해서 books 배열을 만듦

 

9. Books.js에 사용자 이벤트를 통해 정의한 액션이 실행 되도록 처리

- actions에서 addBook, react-redux에서 connect를 import

- name과 author필드를 포함하는 initialState 생성

- updateInput 함수 추가

updateInput = (key, value) => {
    this.setState({
      ...this.state,
      [key]: value
    })
  }

* 텍스트가 입력 될 때 마다 불리며 현재 상태(...this.state)에 새로 업데이트 된 key:value로 업데이트 하여 state를 갱신한다고 이해하면 됨

 

- addBook 함수를 정의, connect를 통해 this.props.dispatchAddBook(this.state)를 호출 할 수 있도록 함

- 위에서 사용할 dispatchAddBook(actions.js의 addBook을 사용)을 connect로 연결하기 위해 아래와 같이 정의

const mapDispatchToProps = {
  dispatchAddBook: (book) => addBook(book)
}

- export default connect(mapStateToProps, mapDispatchToProps)(Books) 

* addBook action을 사용할 수 있도록 mapDispatchToProps를 connect에 전달

 

리덕스를 통해 데이터 레이어를 분리하고 자식 컴퍼넌트에서 데이터와 액션을 사용할 수 있도록 하는 과정은 아래와 같습니다. 

1. 리듀서를 생성하여 사용할 데이터를 정의

2. App.js에 1의 root reducer를 통해 store를 생성, <Provider> 로 store를 하위 component (Books.js)에 전달

3. 하위 component (Books.js)에서 connect를 통해 store를 참조(this.props을 통해 리듀서의 데이터와 함수를 참조 할 수 있다.)

4. actions.js에 리덕스 데이터를 이용한 비지니스 로직인 액션 추가

5. 리듀서에서 4에서 정의한 액션을 통해 데이터가 업데이트 될 수 있도록 정의

6. Component (Books.js)에서 사용자 이벤트를 받아 4에서 정의한 액션이 호출 될 수 있도록 connect를 사용

 

실행결과 : 내용을 입력하고 +를 누르면 목록에 추가 됨

 

더해서 추가된 책을 삭제하는 액션을 추가해 보겠습니다. 

 

10. bookReducer.js에 삭제를 위한 로직 추가

* npm i uuid -save 를 통해 id 발급(uuidV4) 모듈 추가 

- uuid/v4에서 uuidV4 import

- books 객체에 id : uuidV4() 로 아이디 필드 추가

- REMOVE_BOOK 케이스 처리를 위한 코드 추가

case REMOVE_BOOK:
            const index = state.books.findIndex(book => book.id === action.book.id)
            return {
                books: [...state.books.slice(0, index), ...state.books.slice(index+1)]
            }

books 목록에서 action을 통해 넘어온 id로 책의 index를 찾고 index에 해당하는 book만 빼고 앞뒤로 잘라 붙여 새로운 books를 리턴

 

11. actions.js에 uuid, 삭제 로직 추가

- REMOVE_BOOK export, uuid/v4 import

- ADD_BOOK의 book에 id: uuidV4()로 id 필드 추가

- addBook과 비슷하게 removeBook 정의

 

12. Books.js에 removeBook을 호출 하도록 추가

* 전반적으로 addBook과 비슷한 순서로 진행된다. 처음 addBook을 추가할 때 이해를 잘 해두면 두번은 쉽다?

- removeBook 함수를 추가하고 Remove 텍스트를 눌렀을때 선택된 book객체가 전달될 수 있도록 render에 아래 내용 추가

books.map((book, index) => (
              <View style={styles.book} key={index}>
                <Text style={styles.name}>{book.name}</Text>
                <Text style={styles.author}>{book.author}</Text>
                <Text onPress={() => this.removeBook(book)}>Remove</Text>
              </View>
            ))

- mapDispatchToProps에 dispatchRemoveBook 키로 removeBook 액션이 실행 되도록 connect

 

책 목록의 Remove를 누르면 삭제 된다.

 

지금까지 리덕스를 통해 데이터 정의, 비지니스 로직, 뷰 컴퍼넌트를 따로 개발할 수 있도록 레이어 분리하는 과정을 실습해 보았습니다.

 

MVVM 처럼 모델(데이터), 뷰, 비지니스 로직을 분리하는 방식의 아키텍처에 대한 이해가 없다면 왜 이렇게 복잡하게 개발을 해야 하는지 이해하기 힘들 수도 있습니다. 

 

어쨌든 학교에서 실습하듯이 모델, 뷰, 비지니스 로직을 한코드에 때려넣으면 프로젝트의 규모가 커짐에 따라, 개발 팀원들이 모듈을 나눠 개발 하는데 있어 엄청난 부담이 될 수 있습니다. 

 

참고하셔서 요번 포스팅의 실습 내용은 그 구조와 개발 방식을 꼭 이해하고 넘어가시길 바랍니다. 

'React native' 카테고리의 다른 글

크로스 플랫폼 - AppState API  (0) 2020.01.12
크로스 플랫폼 - Alert dialog  (0) 2020.01.10
Animation  (0) 2020.01.05
Navigation  (0) 2020.01.02
고급 Style  (0) 2019.12.29

화면을 전환하거나 로딩 indicator등을 구현할 수 있는 Animation을 구현해봅니다.

 

내장 Animated API를 사용하고 이를 이용해 효과를 실행하는 함수를 만들고 특정 이벤트에 앞서 정의한 Animated 함수가 실행 되도록 합니다. 

 

첫번째 예제는 버튼을 눌렀을때 특정 Component를 이동 시키는 애니메이션 입니다.

 

기본 animated App.js

- marginTop = new Animated.Value(20) //Animated를 위한 변수를 생성, 연결하고 이를 아래 Animated.View component에 연결

- Animated.timing(시작 값:marginTop, 설정객체{종료값, 시간})

- <Animated.View> Component 사용 (View, Image, ScrollView, Text 사용가능)

 

App.js를 실행 후 Animate Box를 누르면 사각형이 이동

두번째 예제는 InputText가 Focus, Blur 이벤트에 따라 width를 변경하는 애니메이션을 적용해 보겠습니다. 

 

InputApp.js

- animatedWidth = new Animated.Value(200) //초기 너비를 200으로 Animated 값으로 지정

- 너비 값을 파라미터로 갖는 animate 함수를 정의하고 Animated.timing을 호출 하도록 함

- <Animated.View>의 스타일에 width를 animatedWidth로 지정

- <TextInput>의 onBlur, onFocus 이벤트 핸들러에 animate 메서드를 연결

* ref={input => this.input = input} //input이라는 이름으로 component의 참조 변수 생성

 

InputText에 Focus를 주면 너비가 커지고 Submit을 누르면 blur가 호출되면서 다시 작아짐

다음은 Animated.loop를 이용한 loading indicator를 생성해 보겠습니다.

 

interpolate 메서드를 사용하고 이는 inputRange(원래 애니메이션 효과), outputRange(앞으로 변경될 효과) 설정 객체를 인수로 사용합니다.

 

Easing : 애니메이션의 움직임을 조정(가,감속 효과), 요번 예에서는 회전 속도가 일정하도록 linear easing 사용

 

 LoopApp.js

- setTimeout(() => this.setState({loading: false}), 2000) //2초뒤 애니메이션 종료를 위한 타이머

const rotation = this.animatedRotation.interpolate({
            inputRange: [0,1],   //애니메이션 속도
            outputRange: ['0deg', '360deg'] //360도 회전
        })

 

여러 Component animation 만들기

 

제목 그대로 다수의 component에 애니메이션 효과를 주고 싶을 때가 있습니다. 병렬(Parallel), 순차(Sequence), 간격(Stagger)

* 예제를 하나하나 치는 입장에서는 그러고 싶지 않습니다. 

여기서는 병렬 처리에 대한 애니메이션만 직접 해보고 나머지는 비슷한 방식에 함수만 적절히 사용하시면 됩니다.

 

Parallel.js

- 다수의 Animated.Value 정의

- animate 함수 내 Animated.parallel([ Animated.Value ... ]).start() 정의

- render내 Component 추가

 

코드는 처음 작성했던 애니메이션 코드를 베이스로 호출하는 함수를 Animated.parallel, 인자로 여러 Component를 배열로 주고 있는 정도의 차이입니다. 

* 순차 처리의 경우 Animated.sequence(Animated.Value), 간격 처리의 경우 Animated.stagger(시간간격, Animated.Value)를 사용하시면 됩니다.

 

Animated.Value의 delay 속성 때문에 각 Component가 순차적으로 애니메이션이 실행 되는 듯이 보임

 

애니메이션 사용 팁

1. 애니메이션 효과 재지정

animate = () => {
       this.animatedValue.setValue(300)
       ...
   }

2. 애니메이션 종료 후 콜백함수

Animated.timing(
         this.animatedTitle,
         {
           toValue: 200,
           duration: 800,
         } 
       ).start(() => console.log('animation is complete!'))

3. 네이티브 UI 스레드에서 애니메이션 실행

React native 기본적으로 자바스크립트 스레드에서 애니메이션이 실행되어 느려지거나 동작을 건너뛰어 버리는 이슈가 발생할 수 있습니다. 따라서 동작에 부하가 큰 애니메이션은 네이티브 UI에서 실행해 주는게 안전합니다. 

* 현재 flexbox, margin, padding 속성에는 이 기능을 사용할 수 없습니다.

Animated.timing(
         this.animatedTitle,
         {
           toValue: 200,
           duration: 800,
           useNativeDriver: true
         } 
       ).start()

4. 사용자 애니메이션 Component 만들기

앞에서 언급했듯이 현재 View, Text, Image, ScrollView에만 애니메이션을 적용할 수 있으나 아래 코드 처럼 정의하면 사용자 Component를 생성하면서 애니메이션 속성을 부여할 수 있습니다. 

const Button = Animated.createAnimatedComponent(TouchableHighlight)
<Button onPress={somemethod} style={styles.button}>
    <Text>Hello</Text>
</Button>

 

이상으로 Reactive native의 animation 효과에 대해서 알아보았습니다.

 

다음으로는 로컬 데이터를 관리하는 redux에 대해서 알아보겠습니다. 

'React native' 카테고리의 다른 글

크로스 플랫폼 - Alert dialog  (0) 2020.01.10
Redux - 데이터 처리  (0) 2020.01.07
Navigation  (0) 2020.01.02
고급 Style  (0) 2019.12.29
Style  (0) 2019.12.27

React Navigation library를 이용하여 탭과 스택 방식의 네비게이션을 구현할 예정입니다. 

 

아래 명령어를 통해 필요한 개별 모듈들을 설치합니다. 

 

npm install --save react-native-gesture-handler react-native-reanimated

npm install --save uuid react-navigation react-navigation-stack react-navigation-tabs

 

*react-native의 버전이 0.60 이하라면 react-native link react-native-gesture-handler 처럼 외부 모듈을 링크 해주는 작업을 추가

 

소스파일 구조

https://github.com/action-intent/ReactNative/tree/master/navigator

위 깃허브의 코드 구조를 참고해 주세요.

*프로젝트 생성시 추가되는 index.js는 navigator의 CitiesApp.js을 실행할 수 있도록 수정

 

앱의 전체 테마 색을 위한 theme.js 추가

primary color를 설정하여 색상 변경 시 쉽게 적용할 수 있도록 함

 

index.js 생성

*navigation을 위한 index.js임 프로젝트 생성시 만들어지는 index.js와 다름

createStackNavigator(라우트 설정 관련 인수, 스타일)

 navigationOptions : 화면 상단에 타이틀과 back 버튼

 

CitiesApp.js 생성

cities state : 이름, 나라, 장소배열, ID 속성을 가진 도시 정보 배열

addCity : 도시 정보를 추가 하기 위한 메서드

addLocation : 각 도시의 위치 정보를 추가하기 위한 도시 객체 내 위치 정보 배열

render : Tabs component를 리턴하고 도시 데이터 및 위 두 메서드를 props로 전달

 

components/CenterMessage.js 빈 화면 표시를 위한 stateless component 추가

 

AddCity.js 도시 추가를 위한 화면 추가

submit : 입력된 값이 비어있지 않으면 uuidV4를 이용해 ID를 할당, 빈 locations 배열 생성, addCity를 호출하여 추가된 city 정보 추가

navigate를 호출하여 추가된 Cities 탭으로 이동

 

도시 목록을 표시하는 Cities.js 추가

- CenterMessage를 가져와 빈 리스트 표시

- static navigationOption을 통해 title(rout)바 속성을 설정

- navigate method를 통해 도시 아이템 선택 시 도시 정보를 인자로 City라우트(navigation.state.params)

- render에서는 scrollView를 통해 도시 목록을 보여주고 TouchableWithoutFeedback을 통해 선택한 도시로 이동

 

각 도시의 주요 장소를 추가하고 출력하는 City.js 추가

- navigationOptions에서 route 객체 반환을 위해 콜백 함수 이용

- addLocation method는 navigation param에서 도시 정보를 가져오고 screenProps의 addLocation을 호출하여 location을 추가

- render에서 도시의 location정보를 scrollView를 통해 출력하고 location을 추가할 수 있는 폼을 추가

 

손가락이 저릴 정도로 코드를 추가 했다. 실행해보자. 동작은 잘 되지 않는다. 인터페이스만 확인하자

 

*실행 시 에뮬레이터에서 아래와 같은 메세지와 함께 실행이 되지 않는다면 아래 빌드 터미널을 종료하고 다시 실행해보자.

invariant violation: module rcteventemitter is not a registered callable module (calling receivetouches) callfunctionreturnflushedqueue [native code]:0

실행 결과 화면

*에뮬레이터에서 경고 창 무시하기 react-native run-ios --varient-release 로 실행하면 됨.

 

데이터 유지하기(AsyncStorage)

앱이 종료된 뒤에도 입력한 데이터를 유지할 수 있도록 캐쉬 모듈을 사용합니다.

 

npm install @react-native-community/async-storage

위 명령어를 통해 AsyncStorage 모듈을 설치합니다. AsyncStorage는 안드로이드의 SharedPreference와 비슷하다고 보시면 됩니다. 

 

*설치 후 아래와 같은 메세지와 함께 run-ios가 되지 않으면 프로젝트 내 ios폴더에서 pod install로 에뮬레이터에 모듈을 올려준다.

error Could not find the following native modules: RNCAsyncStorage. Did you forget to run "pod install" ?

 

CitiesApp.js

1. AsyncStorage를 import

2. 저장 key를 정의하고 componentDidMount 메서드에서 캐쉬된 데이터를 불러온다.

3. 데이터 갱신을 위해 addCity, addLocation 메서드에 JSON.stringify를 이용해 cities를 캐쉬 할 수 있도록 한다.

앱을 재실행 하여도 입력한 도시 정보가 유지된다.

 

이상으로 Navigation, Style적용 및 데이터 캐쉬를 위한 예제를 마무리 하였습니다. 

 

어렵더라도 예제 코드를 이해하고 넘어 갈 수 있도록 합니다. 

 

Tab navigation외 stack, drawer navigation이 있는데 찾아서 연습해 봅시다. 

https://reactnavigation.org/docs/en/next/drawer-based-navigation.html#docsNav

'React native' 카테고리의 다른 글

Redux - 데이터 처리  (0) 2020.01.07
Animation  (0) 2020.01.05
고급 Style  (0) 2019.12.29
Style  (0) 2019.12.27
Todo 2  (0) 2019.12.25

+ Recent posts