배경
여러 번의 리젝과 끝없는 기다림 끝에 개인 프로젝트인 dev-feed을 playstore와 appstore에 배포하는데 성공했다...!
그러나 배포가 끝이 아니다.
나는 앞으로도 컨텐츠를 추가할 생각이다.
그중에 하나가 인기 블로그를 제공하는 것인데, 우선 데이터를 모아보고 싶어 analytics로 해당 데이터들을 모으고자 했다.
dev-feed는 react-native로 만들었기에 @react-native-firebase/analytics를 사용하여 google-analytics를 통해 어떤 블로그를 구독했는지를 수집하기로 했다.
그런데, 조금 있다가, 인기 글도 제공하고 싶어졌다.
그래서 뒤늦게 코드를 추가하려다보니, 글조회, 구독 등등... 전반적인 앱의 핵심 기능과 로깅이 붙어있는걸 발견할 수 있었다.
이렇게 관리해서는 이후에 다른 로깅을 추가하고 싶어질때, 추가하는 속도가 현저히 떨어지는 것이 불보듯 뻔했다.
관심사 분리와 레스토랑
관심사 분리는 레스토랑과 비슷하다고 생각한다.
내가 요리를 잘하고, 몸이 컴퓨터 연산속도 만큼 날래서 혼자서 1인 레스토랑을 오픈했다고 했을때,
물론 나혼자 요리를 다하고, 서빙도하고, 테이블 정리도 하고, 계산도 할 수 있을 것이다.
하지만, 더 많은 요리가 추가되고, 요리가 서빙나갈때, 챙겨야하는 것들, 테이블 정리시에 주의해야할것들, 계산시 할인, 단골 혜택 부여 등등...
점점 신경써야하는 것들이 많아지게되면 혼자서는 실수가 많아지고,레스토랑이 아무리 잘되어도 레스토랑을 더 확장하기 어려울 것이다.
특히나 그런 절차가 조금씩 바뀌기 시작하면... 실수를 할 수 밖에 없게된다.
그래서 보통 규모가 있는 레스토랑은 직원들에게 역할을 부여하고, 직원들은 그 역할을 잘 수행한뒤, 다른 역할을 지닌 사람에게 일을 넘긴다.
예컨데 요리사는 요리만한다. 계산을 하지도 않고, 테이블을 치우지도 않는다.
코드도 마찬가지다. 많은 로직을 다짜내고 어떻게든 성공적으로 돌아가면 그 코드 덩어리 하나로 애플리케이션이 동작할 수 있다.
하지만 요리사도 사람이고, 프로그래머도 사람이다. 더 많은 기능, 수정사항을 반영하기 시작하면 실수를 할 수 밖에 없다.
이런 문제를 해결하기위해서는 관심사를 분리하고, 해당 코드가 걸맞는 역할만을 하게 만드는 것이 옳다고 할 수 있다.
의존성 문제
또한 로깅의 의존성 문제도 있었다. GA4를 내가 잘 사용하지 못하는 것일 수 도 있지만, 아무리 탐색탭을 통해 보고서를 만들어도,
기간별 데이터를 쉽게 조회하거나, 로우데이터 자체를 얻기는 힘들었다.
GA4 사용법을 배워나가고는 있지만, 정말 안되겠다 싶을때, 로그서버로 대체해볼 생각도 있다.
그러려면 퍼져있는 analytics코드를 한곳에 모아줄 필요가 있다.
코드 작성하기
type설계하기
우선 로깅할 이벤트를 타입으로 작성해주었고, 이벤트마다 필요한 파라미터를 정의해주었다.
LogEvent를 통해 params에 정의된 key값을 이벤트 종류로 받아올 수 있다.
의존성 모으기
analytics를 사용해서 로깅하는 코드를 log.ts 모두 모아주었다.
그리고 이렇게 만든 함수를 객체 하나에 묶어 내보내주었다.
이러면 로깅 플랫폼을 GA4에서 자체 로그서버로 바꿨을때, analytics부분만 변경해주면 쉽게 변경할 수 있다.
관심사 분리하기
이게 고민했던 부분이다.
로깅과 비즈니스 로직을 어떻게 구분할까?
페이지 전환
페이지 전환의 경우 root의 NavigationContainer에 이벤트를 주고, 이전 경로와 변경이 일어났을때, 로깅하게 만들면 페이지 전환은 더이상 신경쓸 필요가 없다.
다른 로깅
그런데, 앞서 로깅하려고 했던 블로그 구독 로깅, 글 조회로깅은 어떻게 비즈니스 로직과 구분할 수 있을까
이런 로깅은 공통적으로 사용자의 상호작용, 그중에서도 클릭에 의해 발생하는 이벤트를 기록하는 것이다.
따라서나는 이를 LogClick이라는 컴포넌트로 만들고, 이벤트 종류에 따라 다르게 동작하게 만들어줄 것이다.
이전에 설계한 타입을 기반으로 LogClick에 전달해줄 props타입을 정의해준다.
간단하게 로깅할 이벤트 종류를 뜻하는 logType과 그 타입에 맞는 data를 props로 받도록 해주었다.
type으로 정의해두었기에, typescript에 의해 추론이 가능해진다.
export type LogParamsType = {
subscribe: string;
unsubscribe: string;
viewPost: Article;
screen: {
screenName: string;
screenClass: string;
};
};
export type LogEvent = keyof LogParamsType;
//여기까지 이전에 설계한 type
interface LogClickProps<T extends LogEvent> {
logType: T;
data: LogParamsType[T];
}
이제 LogClick을 만들어주었다.
중요한 것은 어떤 컴포넌트인지와 관계없이, children을 그대로 내보내주되, onPress에 로깅 이벤트를 추가해주어야 한다는 것이다.
cloneElement를 통해 children을 복사하고, onPress이벤트를 추가해주는 형태로 구현해주었다.
export default function LogClick<T extends LogEvent>({
logType,
data,
children,
}: PropsWithChildren<LogClickProps<T>>) {
if (!isValidElement(children)) {
return children;
}
const childWithPress = cloneElement(children as ReactElement, {
onPress: () => {
children?.props.onPress();
Log[logType](data);
},
});
return childWithPress;
}
정리
이제 비즈니스 로직과 로깅로직이 분리되었다.
에러처리는 Errorboundary를 통해 분리되어있으므로,
이제 비즈니스 로직은 성공한 경우에 대해서만 다루게되고, 오류는 Errorboundary에 의해, 로깅은 LogClick에 의해 관리되게 된다.
요리사가 더이상 서빙과 계산을 하지 않게 되는 것이다.
Android
https://play.google.com/store/apps/details?id=com.devfeed
iOS
https://apps.apple.com/kr/app/dev-feed/id6737579223