본문 바로가기
프로그래밍/REACT-NATIVE

REACT-NATIVE(EXPO) SNS 프로젝트 시작 (5)

by Lihano 2021. 7. 26.
반응형

서론

 

내가 두 번째로 만들어볼 것은 본 프로젝트의 성공을 위한 예행 연습용 프로젝트다.

여기에서 나는 1. 파이어베이스의 연동 2. react-navigation의 사용 3. react native에서의 redux 사용 을 목표로 한다.

 

react-navigation의 사용은 저번 포스트에서 다루었다.

SwitchNavigation을 사용했기 때문에 편리한만큼 여러 단점도 있었다.

우선 내가 참고로 하는 블로그에서는 props의 전달을 SwitchNavigation을 통해서 하지 않았다.

그래서 SwitchNavigation이 실제로 props의 전달을 어떻게 하는지, 가능은 하는지는 잘 모른다.

 

대신 react-navigation의 가장 기본이 되는 Stack Navigation에서는 구현도 간단하며 props의 전달도 용이하다는 걸 발견했다.

본 프로젝트의 navigation을 Switch로 할지 Stack으로 할지는 좀 더 고민해봐야할 것 같다.

 

❓ 그러면 Switch Navigation을 사용하면서 props의 관리는 어떻게 했나요? ❔

redux로 했다.

그게 이 포스팅의 주내용이다.

 

React Native 환경에서 Redux를 사용하는 것.

이 주제를 가지고 오늘의 포스팅을 진행하도록 하겠다.

 

리덕스(redux)라는 건?

예전에도 리덕스를 통해 간단한 실습 프로젝트를 진행한 적이 있다.

리덕스란 상태 관리 라이브러리이며 상태 관리를 더 효율적으로 하기 위해 존재한다.

 

자. 리덕스에는 우선 스토어(Store)라는 녀석이 있다.

이 스토에는 상자로서 state 값들을 저장하고 있다.

특정 컴포넌트에 귀속되는 게 아니라 중앙에 존재하기 때문에 공급자(Provider) 내부의 컴포넌트라면 누구나 접근가능하다.

 

그리고 리듀서(Reducer)라는 녀석이 별도로 존재한다.

이 놈의 역할은 액션을 받으면 전달받은 액션을 쓱 보고 스토어의 state를 변경한다.

즉, 액션(Action)이란 State 변경을 위해 리듀서에 전달하는 메세지인 것이다.

 

이게 리덕스의 기본 구성이다.

그리고 다음은 리덕스의 3가지 규칙⚠이다.

 

  1. 스토어는 단 하나만 존재한다. 여러개 생성할 수 없다. 대신 리듀서를 여러개 생성할 수는 있다.✅
  2. State는 읽기 전용이다. 스토어에 접근해 직접 수정하면 안된다. 상태는 수정이 아니라 업데이트의 형태로 새 상태 객체를 만들어서 넣어주어야 한다.✅
  3. 모든 변화는 순수 함수로 구성해야한다. 변화를 담당하는 함수는 당연히 리듀스의 함수다. 이게 무슨 말이냐면 리듀서 함수 내부에서 외부 네트워크와 데이터베이스에 직접 접근하면 안된다. 뿐만아니라 현재 날짜를 반환하는 new Date() 함수나 Math.random 함수 등도 사용하면 안된다.✅

 

이게 리덕스의 기본 규칙이다.

조금 깐깐하지만 그만큼 유용하고 편리하다.

 

리덕스 thunk라는 건?

우선 리덕스를 생성하기 전에 redux thunk에 대해서 알아보고 가자. 이건 미들웨어라는 녀석이다.

이건 리덕스를 사용하는 어플리케이션에서 비동기 작업을 처리할 때 가장 기본적인 방법이다.

 

방금 액션이란 리덕스에 State를 변경하기 위한 메세지라고 설명했다.액션 생성 함수라는 건 이 액션이라는 메세지를 작성하는 함수다.

 

하지만 이 액션 생성 함수는 정말 액션만을 반환해야한다. a라는 prop을 받으면 {action:"ACTION", a} 라는 액션 객체를 생성하는 일 밖에 못하는 것이다.

 

redux thunk는 이런 액션 생성 함수에 날개를 달아주는 역할이다.액션 생성 함수가 비동기적으로 구성될 수도 있게 해주고 dispatch getState를 사용할 수도 있게 해준다.

 

참고로 dispatch란 reducer에 action을 전달하는 함수, 즉 그런 식으로 state를 변경하는 함수이며getState는 state를 가져오는 함수다.store에 직접 접근하는 게 금지되어 있기 때문에 이런 함수를 통해서 값을 가져오고 변경시키는 것이다.

 

이 미들웨어는 생성한 후 store의 생성시에 같이 넣어주면 적용된다.

// 리액트 기본 임폴트
import React from "react";
// 리덕스 리덕스 세팅 임폴트
import { createStore, applyMiddleware } from "redux";
// 리액트 리덕스 공급자 생성 임폴트
import { Provider } from "react-redux";
// 미들웨어를 위한 임폴트
import thunkMiddleware from "redux-thunk";
// 스위치 네비게이션을 임폴트
import SwitchNavigator from "./navigation/SwitchNavigator";
// 리듀서를 임폴트
import reducer from "./reducers";
// 미들웨어 생성
const middleware = applyMiddleware(thunkMiddleware);
// 스토어 생성
const store = createStore(reducer, middleware);

export default class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <SwitchNavigator />
      </Provider>
    );
  }
}

먼저 미들웨어와 리듀서를 생성한 후 둘을 이용해 스토어를 생성한 걸 알 수 있다.

이게 기본적인 리덕스 스토어의 생성 방법이다.

 

액션 생성 함수

그러면 바로 reduc thunk의 위력을 실감하도록 하자.

아래는 작성한 액션 생성 함수다.

 

import Firebase, { db } from "../Firebase.js";

// define types

export const UPDATE_EMAIL = "UPDATE_EMAIL";
export const UPDATE_PASSWORD = "UPDATE_PASSWORD";
export const LOGIN = "LOGIN";
export const SIGNUP = "SIGNUP";

// actions

export const updateEmail = (email) => {
  return {
    type: UPDATE_EMAIL,
    payload: email,
  };
};

export const updatePassword = (password) => {
  return {
    type: UPDATE_PASSWORD,
    payload: password,
  };
};

export const login = () => {
  return async (dispatch, getState) => {
    try {
      const { email, password } = getState().user;
      const response = await Firebase.auth().signInWithEmailAndPassword(
        email,
        password
      );

      dispatch(getUser(response.user.uid));
    } catch (e) {
      alert(e);
    }
  };
};

export const getUser = (uid) => {
  return async (dispatch, getState) => {
    try {
      const user = await db.collection("users").doc(uid).get();

      dispatch({ type: LOGIN, payload: user.data() });
    } catch (e) {
      alert(e);
    }
  };
};

export const signup = () => {
  return async (dispatch, getState) => {
    try {
      const { email, password } = getState().user;
      const response = await Firebase.auth().createUserWithEmailAndPassword(
        email,
        password
      );
      if (response.user.uid) {
        const user = {
          uid: response.user.uid,
          email: email,
        };

        db.collection("users").doc(response.user.uid).set(user);

        dispatch({ type: SIGNUP, payload: user });
      }
    } catch (e) {
      alert(e);
    }
  };
};

맨 위의 define types 부분은 단순한 오탈자 방지를 위한 상수 세팅이다.

무시해도 된다.

 

중요한 건 아래 액션 생성 함수다.

이 컴포넌트는 액션 생성 함수들을 한데 모아놓고 export 시킨다.

어느 컴포넌트든지 이 컴포넌트를 import 하고 필요한 액션 생성 함수를 사용할 수 있도록 말이다.

 

주목할 부분은 위의 updateEmail과 updatePassword는 전형적인 액션 생성 함수로서 액션을 생성하는 기능만을 하지만 아래의 함수들은 함수를 리턴한다는 점이다.

 

위에서 언급한대로 리덕스의 규칙에 의하면 리듀스의 함수는 순수 함수로만 세팅되어야 한다.

즉, 리듀스의 함수도 엄청난 한계를 가지며 실질적으로는 State를 변경시키는 기능밖에 없다는 뜻이다.

 

하지만 위의 액션 생성 함수를 보면 액션 생성 함수 자체가 함수를 리턴하기 때문에 엄청난 유연성을 가진다.

사용자의 입장에선 액션 생성 함수 자체에서 dispatch와 getState를 수행하기 때문에 액션 생성 함수를 불러오는 것만으로도 reducer의 접근이 이루어져 별도로 dispatch를 수행할 필요가 없어진다.

 

login()이란 함수를 살펴보면 호출되는 즉시 getState를 통해 이메일과 비밀번호를 가져온다.

그리고 그걸 파이어베이스의 데이터베이스에 저장한 후, 방금 저장한 데이터베이스의 내용을 불러와서  스토어에 저장한다.

 

굉장히 편리하다.

이런 방식은.

 

스토어(Store)의 사용

그러면 이번에는 일반 컴포넌트가 어떻게 스토어에 접근하고 그 내용을 불러올 수 있는지를 알아보자.

조금 구식인 방식이긴 하지만 여기에서는 Connect를 사용했다.

 

이 Connect가 왜 구식이냐하면 useSelectoruseDispatch가 Connect가 더 편리하기 때문이라고 한다.

이에 대해서는 나중에 알아보도록 하자.

 

사실 Connect가 구식 취급을 받는 이유는 이건 Class 컴포넌트의 방식이기 때문이다.

대세는 함수형 컴포넌트! 클래스 컴포넌트의 기존의 방식은 전부 함수형 컴포넌트로 대체된지 오래긴하다🤣

 

그래도 내가 참조한 블로그에는 클래스 컴포넌트로 작성이 되어있었으니까...😑

 

좀 어려운 말을 쓰자면 Connect라는 건 HOC다.

HOC(Higher-Order Component)란 특정 함수 값을 props로 받아와서 사용하고 싶은 경우에 사용하는 패턴으로, 컴포넌트의 로직을 재활용할 때 유용한 패턴이다.

더 편리한 Hook이 대체한 영역이기 때문에 이걸 이해하려 고민할 필요는 없다.

과감하게 패스하자.

 

Connect 함수는 리덕스 스토어 안에 있는 state를 특정 컴포넌트 안에 props로 넣어줄 수도 있고 액션을 dispatch하는 함수를 props로 넣어줄 수 있다.

 

먼저 mapStateToProps를 만들어준다.

이 녀석은 컴포넌트 안에 store의 state를 props로 넣어주는 녀석이다.

 

그리고 mapDispatchToProps를 만들어준다.

이 녀석은 컴포넌트 안에 dispatch 함수를 props로 넣어주는 녀석이다.

 

그리고 connect는 이 함수들을 인자로 받아 특정 컴포넌트와 묶어준다.

그런 역할을 수행하는 함수다.

 

참고를 위해 로그인 컴포넌트의 코드를 보도록 하자.

import React from "react";
import {
  View,
  TextInput,
  StyleSheet,
  TouchableOpacity,
  Text,
  Button,
} from "react-native";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { updateEmail, updatePassword, login, getUser } from "../actions/user";
import Firebase from "../Firebase";

class Login extends React.Component {
  componentDidMount = () => {
    Firebase.auth().onAuthStateChanged((user) => {
      console.log("여기까지는 되나? 1");
      if (user) {
        console.log("여기까지는 되나? 2");
        this.props.getUser(user.uid);
        if (this.props.user != null) {
          console.log("여기까지는 되나? 3");
          this.props.navigation.navigate("Profile");
        }
      }
    });
  };

  render() {
    return (
      <View style={styles.container}>
        <TextInput
          style={styles.inputBox}
          value={this.props.user.email}
          onChangeText={(email) => this.props.updateEmail(email)}
          placeholder="Email"
          autoCapitalize="none"
        />
        <TextInput
          style={styles.inputBox}
          value={this.props.user.password}
          onChangeText={(password) => this.props.updatePassword(password)}
          placeholder="Password"
          secureTextEntry={true}
        />
        <TouchableOpacity
          style={styles.button}
          onPress={() => this.props.login()}
        >
          <Text style={styles.buttonText}>Login</Text>
        </TouchableOpacity>
        <Button
          title="Don't have an account yet? Sign up"
          onPress={() => this.props.navigation.navigate("Signup")}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  inputBox: {
    width: "85%",
    margin: 10,
    padding: 15,
    fontSize: 16,
    borderColor: "#d3d3d3",
    borderBottomWidth: 1,
    textAlign: "center",
  },
  button: {
    marginTop: 30,
    marginBottom: 20,
    paddingVertical: 5,
    alignItems: "center",
    backgroundColor: "#F6820D",
    borderColor: "#F6820D",
    borderWidth: 1,
    borderRadius: 5,
    width: 200,
  },
  buttonText: {
    fontSize: 20,
    fontWeight: "bold",
    color: "#fff",
  },
  buttonSignup: {
    fontSize: 12,
  },
});

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators(
    { updateEmail, updatePassword, login, getUser },
    dispatch
  );
};

const mapStateToProps = (state) => {
  return {
    user: state.user,
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Login);

코드가 상당히 길지만 돔 구조와 스타일 영역을 제외하면 얼마 없다😅

주목할 부분은 마지막 부분이다.

mapDispatchToProps와 mapStateToProps는 저런 식으로 만들어지며 저런 식으로 Connect에 컴포넌트와 묶인다.

 

특별히 언급할 부분은 없다.

참고로 bindActionCreators는 mapDispatchToProps 내부의 dispatch 함수를 생성하는 과정을 쉽게 해준다.

여러 개의 액션의 수를 다룰 때 유용하다.

 

그리고 connect의 인자의 순서를 잘 보자.

mapStateToProps가 먼저고 mapDispatchToProps가 나중이다.

두번째 인자는 생략될 수 있지만 순서는 틀려선 안된다.

 

오늘은 이렇게 Expo에서 리덕스를 사용해봤다!

연습은 이정도면 충분하다.

 

다음은 실제로 목표했던 프로젝트를 생성해보도록 하자! 💪💪

반응형

댓글