드디어 ToDoList 항목까지 DB와 연동할 수 있었습니다.
현재 DB에 저장되는 정보는 사용자의 google 계정 info, todos가 있습니다.
app의 개별 데이터는 사용자 id를 title로 갖는 독립적인 테이블의 형태로 저장됩니다.
그렇기에 app 사용자 사이에는 절대 영향을 끼치지 않습니다.
중앙에 실시간으로 반영되는 app별 DB가 완성된 것으로 생각이 듭니다.
무엇보다도 신기했던 점은 noSQL DB를 직접 사용해보니 RDB에 비해 확장성이 좋을 수 밖에 없다는 것이 느껴진다는 점입니다.
그냥 원하는 데이터를 원하는 키워드로 큰 고민 없이 편하게 확장할 수 있었습니다.
각 테이블 사이에 관계가 없기 때문에 독립적으로 구성되고, 각각의 테이블에서 원하는 정보들을 조합해 하나의 app service를 구성할 수 있습니다.
그래서 1, 2차 프로젝트에 비해 3차 프로젝트의 데이터 구조는 생각보다 덜 복잡하지 않을까 생각했습니다.
기껏해야 2가지 정보만을 담고 있는 ToDoList app이지만 어서 큰 프로젝트를 만들어보고 싶어지네요.
드디어 class 형태로 다양한 메서드들을 정의했습니다.
사용자를 고려하는 마음으로 만들다보니 module 내 type file을 다시 import 하는 경우도 있었습니다.
어떤 점이 문제인지는 명확히 모르겠으나, lib 내에서 정의한 type, interface를 굳이 한 번 더 사용하는 느낌이라 석연치 않았던 점은 있었습니다.
그래도 나름 메서드를 만들어두고 사용하다보니 상당히 편한 점이 많았습니다.
login은 한 번 사용하기에 몰랐지만, DB 관련 메서드들은 정말 편했습니다.
database.ts
import database from '@react-native-firebase/database';
import {FirebaseDatabaseTypes} from '@react-native-firebase/database/lib';
type Snapshot = FirebaseDatabaseTypes.DataSnapshot;
export const useDb = class {
dir: string;
constructor(dir: string) {
this.dir = dir;
}
on(
eventType:
| 'value'
| 'child_added'
| 'child_changed'
| 'child_moved'
| 'child_removed',
callback: (data: Snapshot, previousChildKey?: string | null) => void,
): void {
database().ref(this.dir).on(eventType, callback);
}
set(value: any, onComplete?: (error: Error | null) => void): void {
if (onComplete) {
database()
.ref(this.dir)
.set(value)
.catch(res => {
onComplete(res);
return;
});
}
database().ref(this.dir).set(value);
}
update(
value: {[key: string]: any},
onComplete?: (error: Error | null) => void,
): void {
if (onComplete) {
database()
.ref(this.dir)
.update(value)
.catch(res => {
onComplete(res);
return;
});
}
database().ref(this.dir).update(value);
}
remove(): void {
// 사용할 때 신중하게 생각해주세요. DB 테이블을 통째로 날리는 메소드입니다.
database().ref(this.dir).remove();
}
push(value: any): void {
database().ref(this.dir).push(value);
}
};
별거 없어 보이지만, 생각보다 정말 편했습니다.
googleSignIn.ts
import {GoogleSignin} from '@react-native-google-signin/google-signin';
import auth from '@react-native-firebase/auth';
import {WEB_CLIENT_ID} from '../config';
export interface GoogleUserInfo {
idToken: string;
serverAuthCode: string;
scopes: string[];
user: {
email: string;
id: string;
givenName: string;
familyName: string;
photo: string;
name: string;
};
}
export class GoogleSignInMethod {
googleSignIn(setUser: Function): void {
GoogleSignin.configure({
webClientId: WEB_CLIENT_ID,
});
const signIn = async () => {
try {
const userInfo = await GoogleSignin.signIn();
const {idToken} = userInfo;
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
await auth().signInWithCredential(googleCredential);
await setUser(userInfo);
} catch (error) {
console.log(error);
}
};
signIn();
}
googleSignOut(setUser: Function): void {
GoogleSignin.configure({
webClientId: WEB_CLIENT_ID,
});
const signOut = async () => {
try {
await GoogleSignin.signOut().then(() => {
auth().signOut();
});
await setUser(null);
} catch (error) {
console.log(error);
}
};
signOut();
}
}
기존 signUp 과정이 있었던 것을 어떻게 처리할까 고민하다가 그냥 과감히 삭제했습니다.
생각해보니 DB에 저장할지 말지를 결정하면 되는 문제였기 때문에 class 내부에서 해당 관련 내용을 포함하지 않는 것이 KISS 원칙을 준수하는 것 같았습니다.
따라서 최초 로그인 당시의 DB 저장은 해당 클래스 내부에서 판단하는 것이 아닌, 버튼이 눌린 시점의 view에서 판단해야겠다고 생각했습니다.
이에 Login.tsx 파일의 내부도 변경되었습니다.
import React, {useState, useEffect} from 'react';
import {
StyleSheet,
SafeAreaView,
View,
Text,
TouchableOpacity,
} from 'react-native';
import {GoogleSigninButton} from '@react-native-google-signin/google-signin';
import {GoogleUserInfo, GoogleSignInMethod} from '../../utils/googleSignin';
import auth from '@react-native-firebase/auth';
import {useDb} from '../../utils/database';
const checkUser = (signInInfo: any): void => {
const userId = auth().currentUser?.uid;
const dbUserInfo = new useDb(`/users/${userId}`);
dbUserInfo.on('value', snapshot => {
if (!snapshot.val()?.googleInfo && signInInfo) {
dbUserInfo.set({googleInfo: {...signInInfo.user}});
return;
}
});
};
export const SignIn = ({navigation}: any) => {
const gogoleSignInMethod = new GoogleSignInMethod();
const {googleSignIn, googleSignOut} = gogoleSignInMethod;
const [signInInfo, setSignInInfo] = useState<GoogleUserInfo | null>(null);
const {navigate}: any = navigation;
useEffect(() => {
if (signInInfo) {
checkUser(signInInfo);
}
}, [signInInfo]);
const {signInContainer, signInBlock, signOutText} = styles;
return (
<SafeAreaView style={signInContainer}>
<View style={signInBlock}>
{signInInfo && (
<>
<TouchableOpacity
onPress={() => {
navigate('ToDoList');
}}>
<Text style={signOutText}>Go ToDoList</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
googleSignOut(setSignInInfo);
}}>
<Text style={signOutText}>Sign Out</Text>
</TouchableOpacity>
</>
)}
{!signInInfo && (
<GoogleSigninButton
size={GoogleSigninButton.Size.Wide}
color={GoogleSigninButton.Color.Dark}
onPress={() => {
googleSignIn(setSignInInfo);
}}
/>
)}
</View>
</SafeAreaView>
);
};
막상 보니 코드가 좀 길긴 하네요.
그래도 최소한의 기능과 로직만을 포함했다고 생각합니다.
다음엔 어딜 더 줄일지 고민해야겠습니다.
생각보다 이것저것 한건 없는데 빨리 하루가 지나갔습니다.
로직을 재구성하기 위해 나름 그림(?)도 하루종일 그리고, 주말 사이에 읽었던 자료들을 떠올리며 어떻게 구성하는게 맞는지 고민하는데 시간을 많이 쓴 것 같습니다.
안타깝게도 프로젝트의 규모가 상당히 작아 주말 사이에 공부했던 개념을 적용시키는 것은 배보다 배꼽이 더 큰 느낌이기에 포기했습니다.
그래도 조금더 고민해보았던 것은 google 말고 다양한 방식의 social Auth를 적용하기 위해 하나의 SignIn class를 만들어 상속을 활용한 틀을 만들어둘까 했던 점입니다.
다음 프로젝트에는 꼭 반영해봐야겠습니다.
이상으로 ToDoList는 마치도록 하겠습니다.
내일은 RN의 다양한 기능, geolocation 등을 ToDoList에 집어넣어볼까 합니다.
혼종 예약입니다.
그럼 글은 이만 마치도록 하겠습니다. 읽어주셔서 감사합니다.