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

EXPO INSTAGRAM CLONE 만들기 (4)

by Lihano 2021. 8. 6.
반응형

서론

이번 인스타그램 클론 만들기 유튜브 강의를 모두 수료했다.

3일 걸렸구나... 그런데 기능은 구현했는데 UI는 전혀 만지지 않는다.

완성된 결과물의 디자인은 내가 스스로 만져야한다.

 

그리고 결과물이 인스타의 완전한 기능을 구현하는 것도 아니다.

정말 핵심적인 기능 3, 4가지만을 구현했지 그 외의 부수적인 기능은 엉성하거나 생략되었다.

 

하지만 그럼에도 불구하고 이건 정말 좋은 강의다.

디자인이야 내 마음대로 하면 되는 거고 자잘한 기능들이야 손쉽게 추가 가능하다.

 

내가 제일 맘에 든건 파이어베이스의 데이터베이스를 활용하는 방식이다.

이 강의를 보고 나서 나는 전 프로젝트의 데이터베이스 사용법이 너무 난잡했다는 걸 알았다.

 

강의를 보고나서 느낀점이 참 많다.

심지어 함수의 모듈화도 상당히 우수하다.

협업을 위한 코드라는 느낌이 들어서 좋다.

 

이번 포스팅엔 강의가 끝난 기념으로 결과물을 리뷰해보자.

 

데이터베이스 구조

이 강의의 최대 특징은 데이터베이스의 사용법이다.

다른점은 제쳐두고라도 이 점은 꼭 짚고 가고 싶다고 생각했다.

 

우선 데이터베이스는 제일 크게 세가지 컬렉션(카테고리)를 가진다.

  • USERS : 유저들의 정보를 저장한다.
  • POSTS : 포스트들의 정보를 저장한다.
  • FOLLOWING : 팔로우 관계를 저장한다.

각각의 컬렉션은 또 다른 하위 컬렉션을 가진다.

먼저 USERS의 구조는 다음과 같다.

users의 구조는 이해하기 쉽다.

users는 전체 유저들의 정보를 저장하는 컬렉션이다.

각 유저들의 유저 아이디를 이름으로 가지는 문서들을 가지며 각 문서는 이메일과 이름 정보를 가진다.

유저 아이디를 가지고 이름과 이메일 같은 유저 정보를 찾고 싶다면 users 컬렉션을 이용하면 된다.

 

posts의 구조는 다음과 같다.

posts는 앱 전체의 포스트들을 관리하는 컬렉션이다.

보다시피 포스트들은 업로드한 유저의 유저 아이디로 1차 구분된다.

 

유저 아이디 이름의 문서는 userPosts라는 또 하나의 컬렉션을 가진다.

이 컬렉션에는 해당 유저가 올린 포스트들이 문서별로 저장된다.

각 문서의 이름은 포스트 아이디로 지정된다.

 

각 포스트 아이디 이름의 문서는 세가지 정보 필드를 가진다.

  • caption - 텍스트
  • creation - 업로드한 시간 정보
  • downloadURL - 사진URL

그리고 세가지 필드와 함께 하위 컬렉션을 가진다.

likes라는 하위 컬렉션은 해당 포스트를 추천 누른 상대방의 uid를 문서 이름으로 저장한다.

마지막으로 following 컬렉션의 구조다.

following 컬렉션은 앱의 모든 팔로윙 관계 정보를 저장한다.

이 유저가 어떤 사람을 팔로우하고 있는지를 나타낸다.

 

우선 팔로윙 컬렉션은 유저들의 아이디들을 문서로 가진다.

각 유저들의 아이디 문서들은 userFollowing이란 하위 컬렉션을 가진다.

이 컬렉션은 팔로우하고 있는 상대의 유저 아이디를 이름으로 하는 문서들을 가진다.

이 문서 안에는 어떠한 정보도 없다. 그저 이름만 상대의 유저 아이디일 뿐이다.

 

이렇게 특정 uid를 이용해 이 유저가 팔로윙한 상대들의 uid를 얻어올 수 있다.

이렇게 컬렉션 안에 컬렉션을 만드는 방식은 내가 전혀 몰랐던 부분이다.

이렇게 하니 코드가 훨씬 깔끔해지고 데이터의 관리가 훨씬 간편해진다.

좋은 걸 배웠다고 생각한다.

 

리듀스 구조

데이터베이스는 서버가 가지고 있는 정보고,

앱이 실제로 활용하는 정보는 Store에 저장된다.

 

외부(database)의 정보를 가져와서 내부(store)에 저장할 필요가 있다는 뜻이다.

데이터베이스가 정보를 저장하는 구조와 앱이 정보를 저장하는 구조는 다르다.

 

우선 이 앱은 두 개의 리듀스를 가진다.

물론 바인드 되어 하나의 리듀스로 묶이지만 두가지의 리듀싱 함수와 store로 나뉘어져있다는 사실을 기억해야 한다.

 

하나는 개인의 정보를 저장하는 user 리듀스다.

나머지 하나는 유저들의 정보를 저장하는 users 리듀스다.

 

user 리듀스는 로그인과 로그아웃 등 현재 계정 정보를 다룰 때 사용된다.

user의 store는 다음과 같이 구성된다.

// 초기 state 값
const initialState = {
  currentUser: null, // 현재 유저
  posts: [], // 현재 유저의 포스트들
  following: [], // 현재 유저의 팔로윙 유저들
};

그리고 users는 유저들의 리스트를 다룰 때 사용한다.

유저들의 리스트는 현재 계정이 팔로우하고 있는 유저들로 구성된다.

users의 store 구조는 다음과 같다.

const initialState = {
  users: [], // 유저 리스트
  feed: [], // 유저들의 포스트들
  usersFollowingLoaded: 0, // 로드된 유저 수
};

feed는 Feed 페이지에 현재 팔로우한 유저들의 게시물을 로드하기 위해 필요한 정보다.

그리고 usersFollowingLoaded는 숫자 확인용 state다.

현재 로드된 유저 수가 올바른지 검사하는데 사용된다.

 

각 리듀싱 함수의 코드를 들여다보자.

먼저 user의 리듀싱 함수다.

// reducer (user)
export const user = (state = initialState, action) => {
  switch (action.type) {
    case USER_STATE_CHANGE:
      return { ...state, currentUser: action.currentUser };
    case USER_POSTS_STATE_CHANGE:
      return { ...state, posts: action.posts };
    case USER_FOLLOWING_STATE_CHANGE:
      return {
        ...state,
        following: action.following,
      };
    case CLEAR_DATA:
      return {
        currentUser: null,
        posts: [],
        following: [],
      };
    default:
      return state;
  }
};

보면은 각 리듀서가 수행하는 동작을 이해할 수 있다.

  • USER_STATE_CHANGE : 현재 로그인된 유저(currentUser) 정보를 업데이트
  • USER_POSTS_STATE_CHANGE : 현재 로그인된 유저의 포스트 리스트(posts)를 업데이트
  • USER_FOLLOWING_STATE_CHANGE :  현재 유저가 팔로우한 유저의 리스트(following)를 업데이트

보면 어떤 정보를 업데이트하는지 일목요연하게 이해가 가능하다.

이 리듀싱 함수는 현재 계정주인 개인 계정 정보만을 업데이트하고 있다.

 

반면 users는 복수의 user들의 정보를 다룬다.

아래는 users의 리듀싱 함수다.

export const users = (state = initialState, action) => {
  switch (action.type) {
    case USERS_DATA_STATE_CHANGE:
      return {
        ...state,
        users: [...state.users, action.user],
      };
    case USERS_POSTS_STATE_CHANGE:
      return {
        ...state,
        usersFollowingLoaded: state.usersFollowingLoaded + 1,
        feed: [...state.feed, ...action.posts],
      };
    case USERS_LIKES_STATE_CHANGE:
      return {
        ...state,
        feed: state.feed.map((post) =>
          post.id == action.postId
            ? { ...post, currentUserLike: action.currentUserLike }
            : post
        ),
      };
    case CLEAR_DATA:
      return initialState;
    default:
      return state;
  }
};
  • USERS_DATA_STATE_CHANGE : 현재 유저 리스트(팔로우 된)를 업데이트한다.
  • USERS_POSTS_STATE_CHANGE : 피드(팔로우 유저들의 포스트)를 업데이트 한다.
  • USERS_LIKES_STATE_CHANGE : 피드 중 특정 게시물의 좋아요 상태를 업데이트 한다. 

users 리듀서가 state를 어떻게 다루는지 알 수 있다.

이 리듀서는 액션 생성 함수로부터 dispatch 요청을 받아서 state를 업데이트한다.

그렇다면 action 생성 함수는 어떤 함수로 이루어져 있는 걸까?

 

다음은 action 페이지의 코드다.

export function clearData() {
  return (dispatch) => {
    dispatch({ type: CLEAR_DATA });
  };
}

export function fetchUser() {
  return (dispatch) => {
    firebase
      .firestore()
      .collection("users")
      .doc(firebase.auth().currentUser.uid)
      .get()
      .then((snapshot) => {
        if (snapshot.exists) {
          dispatch({ type: USER_STATE_CHANGE, currentUser: snapshot.data() });
        } else {
          console.log("does not exist");
        }
      });
  };
}

export function fetchUserPosts() {
  return (dispatch) => {
    firebase
      .firestore()
      .collection("posts")
      .doc(firebase.auth().currentUser.uid)
      .collection("userPosts")
      .orderBy("creation", "asc")
      .get()
      .then((snapshot) => {
        let posts = snapshot.docs.map((doc) => {
          const data = doc.data();
          const id = doc.id;
          return { id, ...data };
        });
        dispatch({ type: USER_POSTS_STATE_CHANGE, posts });
      });
  };
}

export function fetchUserFollowing() {
  return (dispatch) => {
    firebase
      .firestore()
      .collection("following")
      .doc(firebase.auth().currentUser.uid)
      .collection("userFollowing")
      .onSnapshot((snapshot) => {
        let following = snapshot.docs.map((doc) => {
          const id = doc.id;
          return id;
        });
        dispatch({ type: USER_FOLLOWING_STATE_CHANGE, following });
        for (let i = 0; i < following.length; i++) {
          dispatch(fetchUsersData(following[i], true));
        }
      });
  };
}

export function fetchUsersData(uid, getPosts) {
  return (dispatch, getState) => {
    const found = getState().usersState.users.some((el) => el.uid === uid);
    if (!found) {
      firebase
        .firestore()
        .collection("users")
        .doc(uid)
        .get()
        .then((snapshot) => {
          if (snapshot.exists) {
            let user = snapshot.data();
            user.uid = snapshot.id;

            dispatch({ type: USERS_DATA_STATE_CHANGE, user });
          } else {
            console.log("does not exist");
          }
        });
      if (getPosts) {
        dispatch(fetchUsersFollowingPosts(uid));
      }
    }
  };
}

export function fetchUsersFollowingPosts(uid) {
  return (dispatch, getState) => {
    firebase
      .firestore()
      .collection("posts")
      .doc(uid)
      .collection("userPosts")
      .orderBy("creation", "asc")
      .get()
      .then((snapshot) => {
        const uid = snapshot.docs[0].ref.path.split("/")[1];
        const user = getState().usersState.users.find((el) => el.uid === uid);
        let posts = snapshot.docs.map((doc) => {
          const data = doc.data();
          const id = doc.id;
          return { id, ...data, user };
        });
        for (let i = 0; i < posts.length; i++) {
          dispatch(fetchUsersFollowingLikes(uid, posts[i].id));
        }
        dispatch({ type: USERS_POSTS_STATE_CHANGE, posts, uid });
      });
  };
}

export function fetchUsersFollowingLikes(uid, postId) {
  return (dispatch, getState) => {
    firebase
      .firestore()
      .collection("posts")
      .doc(uid)
      .collection("userPosts")
      .doc(postId)
      .collection("likes")
      .doc(firebase.auth().currentUser.uid)
      .onSnapshot((snapshot) => {
        console.log(snapshot);
        const postId = snapshot.ref.path.split("/")[3];
        console.log(postId);

        let currentUserLike = false;
        if (snapshot.exists) {
          currentUserLike = true;
        }

        dispatch({ type: USERS_LIKES_STATE_CHANGE, postId, currentUserLike });
      });
  };
}

리듀싱 함수가 단순히 state를 변경 시키는 역할만을 수행한다면 액션 생성 함수는 좀 더 구체적인 동작을 지시한다.

예를 들어 fetchUser 함수의 경우,

  1. 파이어베이스의 인증 서비스로부터 현재 로그인된 계정의 유저 아이디를 받아온다.
  2. 파이어베이스로의 데이터베이스로의 user 컬렉션으로부터 받아온 유저 아이디의 정보를 가져온다.
  3. 가져온 계정 정보를 dispatch를 통해 user 스토어의 currentUser 상태에 저장한다.

이런 동작을 수행한다.

훨씬 구체적이고 모듈적인 동작이 가능하다.

 

그리고 fetchUserPosts

  1. 파이어베이스의 인증 서비스로부터 현재 로그인된 계정의 유저 아이디를 받아온다.
  2. 파이어베이스의 데이터베이스 서비스의 posts 컬렉션에서 해당 유저 아이디의 문서를 찾는다.
  3. 문서 안의 userPosts 컬렉션의 문서들을 creation 순으로 정렬하여 받아온다.
  4. 받아온 포스트 리스트들을 dispatch를 통해 user 스토어의 posts 상태에 저장한다.

그 다음 fetchuserFollowing은 현재 로그인된 유저 아이디를 이용해 팔로우한 유저들의 아이디 리스트를 받아와서 user의 following 상태에 저장한다.

 

users의 액션 생성 함수는 유저 리스트의 정보를 채우기 위한 기능들로 구성되어 있다.

users의 액션 생성 함수들 자체는 특정 uid를 이용해 그 계정의 정보를 받아오는 식으로 구성되어 있다.

 

예를 들어 fetchUserData는 특정 uid를 이용해 그 계정의 email과 data를 받아온다.

그리고 fetchUserFollowingPosts는 특정 uid의 포스트들을 받아온다.

 

fetchUsersFollowingLikes는 데이터베이스에서 특정 uid의 특정 postId의 likes컬렉션을 조회하여 지금 로그인 된 유저가 그 게시물에 좋아요를 눌렀는지 안눌렀는지 여부를 조사해 스토어에 반영한다.

 

정리

설명을 어렵게 했지만 쉽게 말하자면 user는 "나"고 users는 내가 팔로윙한 "다른 유저들"이다.

users가 필요한 이유는 Feed 페이지에 내가 팔로윙한 유저들의 포스트를 띄우기 위함이다.

 

그러니 우선은 앱을 구동하면

  1. user인 나를 식별하고
  2. 표시할 users들의 리스트를 식별한 뒤
  3. users의 포스트 데이터를 받아와야 한다.

필요한 데이터는 데이터베이스에서 리덕스가 꺼내쓴다.

액션 생성 함수는 onSnapshot 함수 또한 사용한다.

이는 데이터베이스의 변화를 감지하여 실시간으로 리덕스를 업데이트한다.

 

Main 페이지를 구동할 때 리덕스에서 액션 생성 함수를 받아오고 실행시키면 필요한 초기 데이터를 전부 로드할 수 있다.

이때 모든 함수를 실행시킬 필요는 없다.

액션 생성 함수는 기능에 따라 분류했을 뿐이지, 연동되어 하나의 작업으로 사용되는 것도 있기 때문이다.

 

예를 들어 현재 팔로우 유저들의 uid를 구하는 fetchUserFollowing은

→ 팔로우 유저들의 계정 정보를 구하는 fetchUsersData와

→ 팔로우 유저들의 포스트를 구하는 fetchUsersFollowingPosts와

→ 내가 이 포스트에 좋아요를 눌렀는지 여부를 구하는 fetchUsersFollowingLikes로 이어지기 때문이다.

 

따라서, 모든 함수에 onSnapshot을 사용해줄 필요도 없고,

초기화 작업이 필요한 페이지에 모든 함수를 명시해줄 필요도 없다.

 

Firebase Cli

Firebase Cli는 파이어베이스 프로젝트를 관리, 조회, 배포할 수 있는 다양한 도구를 제공한다.

설치는

npm install -g firebase-tools

이렇게 가능하다.

이 명령어는 디렉토리에 구애받지 않고 사용 가능한 firebase 명령어를 사용 설정한다.

설치하려면 파이어베이스에 로그인 해야한다.

firebase login

이 명령어를 수행하면 로그인을 진행하여 인증 작업을 거치게 해준다.

firebase projects:list

이 명령어를 입력했을 때 내 파이어베이스 콘솔 창의 프로젝트 이름들이 나열되면 잘 설치된 것이다.

 

firebase cli의 기능을 사용하기 위해서는 firebase init 명령어를 입력하여 프로젝트 디렉터리 설정을 해줘야한다.

firebase einit

그런 후엔 몇가지 질문에 답해줘야한다.

질문은 간단하다.

어떤 서비스를 사용할 것인가?

어떤 프로젝트에 사용할 것인가? 등등이다.

 

이 CLI는 프로젝트를 호스팅하기 위해 많이 사용한다.

firebase deploy

위 명령어를 통해 파이어베이스 프로젝트의 기본 호스팅 사이트에 배포 가능하다.

파이어베이스 프로젝트의 데이터를 로컬의 데이터와 동기화하는 작업이라고 보면 된다.

 

firebase init을 통해 생성한 폴더를 보면 functions과 public이 있다.

  • functions : 디렉토리 안의 index.js 파일을 통해 서버 구동 및 파이어베이스에서 제공되는 URL로 AJAX가 가능
  • public : HTML, CSS, JS 파일을 위치시키면 파이어베이스에서 제공되는 URL로 index.html에 접근 가능

function의 index에 원하는 함수를 작성할 수 있다.

 

Cloud Function

 

이런 파이어베이스의 Cloud Function은 클라우드에 존재합니다.

그러니 어디에서든 앱을 개발할 때 마치 로컬에 존재하는 함수처럼 호출 가능합니다.

 

HTTPS 요청으로 백엔드 코드를 실행할 수 있는거죠.

코드는 구글의 클라우드에 저장되고 실행됩니다.

 

실행 방법에 대해서 알아볼까요? 쉽습니다.

  1. 우선 Cloud Functions에 대한 설정을 완료합니다. 초기 셋팅이란거죠.
  2. 그리고 호스팅 사이트에 대해 HTTPS 함수를 만들어줍니다.
  3. 마지막으로 함수에 HTTPS 요청을 전달합니다.

이런 과정을 통해서 클라우드의 함수를 실행할 수가 있어요.

우선 첫번째 단계인 클라우드 Function의 초기 셋팅은 위에서 다 완료했죠?

 

초기 셋팅을 완료하면 functions이란 폴더가 생겼을거에요.

그게 function 기능을 수행하는 폴더입니다. 들어가요.

그 안의 index.js에 우리는 원하는 함수를 만들어줄 수가 있어요.

 

우리가 이 function 서비스를 이용하는 이유에 대해서 설명해볼까요?

이 클라우드 함수를 사용하면 Node.js 코드를 배포하여 데이터베이스의 변경을 통해 트리거되는 이벤트를 처리할 수 있어요.

덕분에 자체 서버를 실행하지 않고도! 앱에 서버 측 기능을 쉽게 추가할 수 있습니다.

 

이게 무슨 말인가하면, 별도의 코드 실행 없이도 트리거를 작동시킬 수 있다는 겁니다!

기존의 우리는 데이터베이스의 변경을 감시하기 위해서는 파이어베이스의 파이어스토어에 onSnapshot을 요청해야만 했습니다.

하지만 이제 그런 트리거 함수는 우리 코드가 아니라, 클라우드 서버에 올려놓고 자동으로 돌아가도록 시키자구요.

 

클라우드 함수가 처리해주는 이벤트의 유형은 다음과 같습니다.

  • onCreate : 문서를 처음으로 기록할 때 트리거됩니다.
  • onUpdate : 이미 존재하는 문서에서 값이 변경되었을 때 트리거됩니다.
  • onDelete : 데이터가 있는 문서가 삭제되면 트리거됩니다.
  • onWrite : onCreate, onUpdate 또는 onDelete가 트리거될 때 트리거됩니다.

대단하지 않나요?

기존의 onSnapshot은 모든 변화를 트리거해서 CURD의 구분이 불가능했지만 이 클라우드 함수는 각 이벤트를 완벽하게 구별해줍니다.

반응형

댓글