그림은 밥 삼촌의 The Clean Architecture 책에 나오는 다이어그램입니다.
오늘은 꼭 Clean architecture를 적용하겠다는 일념하에 하루종일 코드만 바라보고 있었습니다.
그런데 역시 마법은 일어나지 않았고, 큰 고뇌에 빠지기만 하나 싶던 와중에 가뭄에 단비같은 Office hour 시간이 찾아왔습니다.
무게를 가볍게 하는 것 또한 clean architecture의 본래 목적이었음을 잊었기에 고민이 깊어지지 않았나 생각이 듭니다.
이어서 코드로 보겠습니다.
먼저, repository의 형태입니다.
import database, {FirebaseDatabaseTypes} from '@react-native-firebase/database';
type EventType = FirebaseDatabaseTypes.EventType;
type DataSnapshot = FirebaseDatabaseTypes.DataSnapshot;
export class UsingFirebaseDB {
getDataFromDB(
dir: string,
eventType: EventType,
successCallback: (a: DataSnapshot, b?: string | null) => any,
) {
return database().ref(dir).once(eventType).then(successCallback);
}
setDataToDB(dir: string, value: any) {
return database().ref(dir).set(value);
}
updateDataInDB(dir: string, value: any) {
return database().ref(dir).update(value);
}
}
결과를 생각했을 때, 이러한 코드는 사실 respository 중에서도 베이스가 되는 파일이어야 할 것 같습니다.
repository는 단순히 CRUD 메서드의 추상만 존재하는 것이 아니라 실질적인 접근이 이루어지는 로직을 다루어야 한다.
이어서 useCase에서도 나오겠지만, repository의 무게가 가벼워질수록 useCase에서 처리할 로직이 많아지고, 이는 본래 유연한 변경이 가능해야 한다는 조건과 다른 형태의 App이 된다.
후에 DB를 통째로 바꾸어야 하는 상황에서 어떻게 비교적 간단하게 처리할 수 있을지 고민하는 것으로 repository를 만들자.
등의 조언을 office hour 시간 Q&A를 통해 얻을 수 있었습니다.
이어서 useCase로 넘어가봅시다.
import {Alert} from 'react-native';
import {UsingFirebaseDB} from '../repository/UsingFirebaseDB';
import {
GoogleSignin,
statusCodes,
} from '@react-native-google-signin/google-signin';
import auth from '@react-native-firebase/auth';
import {CLIENT_ID} from '../../../env.json';
export class SignInUseCase extends UsingFirebaseDB {
async getAccessUserEmailFromDB() {
try {
const email = await super.getDataFromDB(
'/access/email',
'value',
snapshot => {
return [...snapshot.val()];
},
);
return email;
} catch {
return null;
}
}
async signInGoogleAuth() {
GoogleSignin.configure({
webClientId: CLIENT_ID,
});
try {
const userInfo = await GoogleSignin.signIn();
const {idToken, user} = userInfo;
const googleCredential = auth.GoogleAuthProvider.credential(idToken);
await auth().signInWithCredential(googleCredential);
return user;
} catch (e) {
if (e.code === statusCodes.SIGN_IN_CANCELLED) {
console.log('유저가 로그인 취소');
} else if (e.code === statusCodes.IN_PROGRESS) {
console.log('로그인 진행 중');
} else if (e.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
console.log('플레이 서비스를 사용할 수 없음');
} else {
console.log('그외 에러');
}
Alert.alert('다시 로그인 해주세요');
return {};
}
}
}
useCase의 로직은 제가 작성한 getAccessUserEmailFromDB
와 Steve가 작성한 signInGoogleAuth
로 이루어져 있습니다.
singleton을 깊게 공부하지 않았기에 적용에 대해서는 아직 잘 모르겠습니다만, 최적화를 고려할 때 필요한 부분에 적용하자는 마인드입니다.
useCase의 문제는 다음과 같았습니다.
repository에서 다룰 수 있는 메서드를 굳이 다루고 있다.
이어서 나오겠지만, 비즈니스 로직을 지나치게 보수적으로 정했다.
정도가 Q&A 시간에 머리에 떠올랐던 것 같습니다.
그럼 이어서 presenter & view를 봅시다.
먼저, presenter입니다.
import React, {useEffect, useState} from 'react';
import {SignInUseCase} from '../model/useCase/SignInUseCase';
import {SignIn} from '../view/SignIn';
interface SignInUser {
id: string;
name: string | null;
email: string;
photo: string | null;
familyName: string | null;
givenName: string | null;
}
const signInLogic = new SignInUseCase();
const {getAccessUserEmailFromDB, signInGoogleAuth} = signInLogic;
const handleSocialSignIn = (
setSignInUserInfo: React.Dispatch<React.SetStateAction<SignInUser | {}>>,
) => {
signInGoogleAuth().then(userInfo => {
setSignInUserInfo(userInfo);
});
};
const loadAccessEmail = (
setAccessUserEmail: React.Dispatch<React.SetStateAction<string[] | null>>,
) => {
getAccessUserEmailFromDB().then(email => {
setAccessUserEmail(email);
});
};
const signInValidation = (
signInUserInfo: SignInUser | {},
accessUserEmail: string[] | null,
) => {
if (signInUserInfo && accessUserEmail?.includes(signInUserInfo.email)) {
return true;
} else {
return false;
}
};
export const SignInPresenter = ({navigation}: any) => {
const {navigate} = navigation;
const [accessUserEmail, setAccessUserEmail] = useState<string[] | null>([]);
const [signInUserInfo, setSignInUserInfo] = useState<SignInUser | {}>({});
useEffect(() => {
if (accessUserEmail !== null && accessUserEmail.length === 0) {
loadAccessEmail(setAccessUserEmail);
}
if (signInValidation(signInUserInfo, accessUserEmail)) {
navigate('Main');
}
}, [accessUserEmail, signInUserInfo]);
return (
<SignIn
handleSocialSignIn={() => {
handleSocialSignIn(setSignInUserInfo);
}}
/>
);
};
제 생각에 useCase로 일부 이전해야 할 부분이 있는 것 같습니다.
예를 들자면, signInValidation, handleSocialSignIn, loadAccessEmail 등의 함수는 useCase에 작성되는게 맞는 것 같습니다.
그 외에는 딱히 없는 것 같으니 넘어가도록 하겠습니다.
이어서 view입니다.
import React from 'react';
import {StyleSheet} from 'react-native';
import {Image, VStack} from 'native-base';
import {GoogleSigninButton} from '@react-native-google-signin/google-signin';
interface ISignInProps {
handleSocialSignIn: () => void;
}
export const SignIn = ({handleSocialSignIn}: ISignInProps) => {
const {googleButton} = style;
return (
<VStack flex={1} bg="white" justifyContent="center" alignItems="center">
<Image
source={require('../../data/images/soomgo_logo_rgb.png')}
alt="Logo"
w="70%"
resizeMode="contain"
marginBottom={75}
/>
<GoogleSigninButton
style={googleButton}
size={GoogleSigninButton.Size.Wide}
color={GoogleSigninButton.Color.Light}
onPress={handleSocialSignIn}
/>
</VStack>
);
};
const style = StyleSheet.create({
googleButton: {
position: 'absolute',
bottom: 90,
},
});
네. view는 view 답네요.
딱히 문제는 없어보이지만, 굳이 문제라면 nativebase와 RN component를 섞어서 사용해야 한다는 점?
그런 부분이 문제가 아닐까 싶습니다.
오늘의 문제
경로, 요청, CRUD를 혼합한 특정 기능에 요구되는 DB 요청 메서드를 만들자.
독립적인 형태로! 가볍게! DRY-KISS는 필수!
코딩을 줄일 수 있다면 줄이는 것이 언제나 고려사항에 있어야한다.
애매모호함을 탈피할 수 있도록 기준을 세우자.
사실 일단 모아두고 나중에 분배하는 방법도 나쁘지 않아보인다.
이제 누가 useCase?
좀 더 문서를 읽어본다면 해결할 수 있지 않을까~
이상입니다.