문제 발생
파이어베이스를 사용한 프로젝트 진행 중 어려움을 겪고 있던 부분이 있었는데
바로 Firestore 사용량...
Firebase를 무료로 이용하는 경우, 하루 데이터 read 수를 50,000회로 제어하고 있다.
보통이라면 이 50,000회를 넘치게 사용하기는 쉽지 않고
실제로 조금 더 대형 프로젝트인 감자마켓도 하루를 제외하고는 일일 5만 건을 넘긴 적이 없었다.
그런데 개인 프로젝트인 내 제트위터🍧가 어느순간부터
24시간 계속 열어두고 작업한 것도 아닌데, 조금만 켜두어도 사용량을 순식간에 다 먹어버리는 거다.
제발.... 제발 그만해 다오.....😱😱😱
해야 할 작업이 산더미인데 자꾸 저러니 프로젝트 진행에 큰 걸림돌이 됐다.
이 참에 전체적인 성능 개선이나 하자하고
SEO 강화, 이미지 최적화와 Lazy Loading 적용 등 여러 가지를 시도하다 보니
Lighthouse의 퍼포먼스 점수를 90점대 기록하는 기염을 토했지만
정작 본작업을 들어갈 수가 없으니 답답함이 있었는데...
드 디 어
명확한 원인을 찾아냈다! 😭
문제 발생 지점 찾아내기
정확히 데이터를 잡아먹는 페이지와 컴포넌트를 잡아내기 위해
시간 간격을 두고 기능을 수행해 보거나, 특정 페이지를 열람한 채로 방지하는 등의 테스트를 진행했다.
5분 동안 홈을 열어뒀다가,
5분 동안 쉬었다가,
5분 동안 글 작성 폼을 열어뒀다가...
그렇게 문제가 발생하는 지점을 특정할 수 있었는데
유저 프로필 페이지 ("/") (Profile)
그리고 그중에서도 유저의 글 목록을 노출하는 타임라인 컴포넌트(UserTimeline)로 좁혀졌다.
로직을 되짚어보며 원인 찾기
UserTimeline 컴포넌트는 현재 사용자가 작성한 포스팅만 골라 보여주는 기능을 한다.
// src/firestore.ts
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app); // Authentication
export const db = getFirestore(app); // Firestore
// src/components/user-timeline.tsx
import { useEffect, useState } from 'react';
import { collection, getDocs, limit, orderBy, query, where } from 'firebase/firestore';
import { auth, db } from '../firebase.ts';
import Tweet from './tweet.tsx';
import * as S from '../styles/timeline.ts';
import ITweet from '../interfaces/ITweet.ts';
export default function UserTimeline() {
const user = auth.currentUser;
const [tweets, setTweets] = useState<ITweet[]>([]);
const fetchTweets = async () => {
const tweetsQuery = query(
collection(db, 'tweets'),
where('userId', '==', user?.uid),
orderBy('createdAt', 'desc'),
limit(25),
);
const snapshot = await getDocs(tweetsQuery);
const tweetList = snapshot.docs.map((doc) => {
const { userId, userName, tweet, createdAt, photo } = doc.data();
return {
userId,
userName,
tweet,
createdAt,
photo,
id: doc.id,
};
});
setTweets(tweetList);
};
useEffect(() => {
fetchTweets();
}, [tweets]);
return (
<S.TimelineWrapper>
{tweets.map((tweet) => (
<Tweet key={tweet.id} {...tweet} />
))}
</S.TimelineWrapper>
);
}
상세하게 로직을 뜯어 확인해 보자면
현재 이용 중인 사용자를 확인해(auth.currentUser)
모든 유저들이 포스팅한 글들을 저장하고 있는 컬렉션 'tweets'에서(collection(db, 'tweets'))
필드 userId의 값이 사용자의 uid와 동일한 문서를 골라(tweetsQuery) 낸 데이터(snapshot.docs)를 가지고
포스팅 목록을 state(tweets)로 저장해 개별 포스트 컴포넌트(Tweet)를 각각 구성해 낸다.
혹시나 데이터를 가져오는 과정에서
실시간으로 데이터 변동을 확인하는 onSnapshot 메서드를 사용하고 있어 문제가 됐나? 의심했었지만
최초 1회에만 데이터를 가져오는 getDocs 메서드를 사용하고 있었기 때문에 관련 없었다.
그럼 대체 뭐가 문제였을까?🤨
답은 useEffect에 있다.
데이터를 가져오는 핵심 함수 fetchTweets는 이 컴포넌트가 렌더링 될 때(mount 될 때) 호출되어야 하기 때문에
useEffect안에 fetchTweets()를 넣어주는 조치를 취했었다.
useEffect(() => {
fetchTweets();
}, [tweets]);
그런데 왜.... dependency에 tweets가 들어가 있지?
😨
fetchTweets 함수를 다시 확인해 보자.
포스트의 각 데이터를 배열 tweetList로 저장한 뒤
setTweets(tweetList);를 통해 state tweets의 값으로 새로 지정해 준다.
그리고 useEffect의 dependency는 useEffect를 호출하기 위한 수단으로
dependency가 빈 배열([])이라면, 최초 렌더링 시에만 1회 useEffect가 실행되고
dependency에 지정된 값이 있다면, 그 값에 변화가 생길 때마다 useEffect가 실행된다.
그렇다면....
하이ㅋ fetchTweets 호출해!
뭐? tweets 값이 바뀌었다고?
그럼 useEffect 한 번 더 실행해야지! fetchTweets 호출해!
뭐? tweets 값이 또 바뀌었다고?
그럼 useEffect 한 번 더 실행해야지! fetchTweets 호출해!
뭐? tweets 값이 또 또 바뀌었다고?
그럼 useEffect 한 번 더 실행해야지! fetchTweets 호출해!
뭐? tweets 값이 또 또 또 바뀌었다고?
....
..
getDocs가 한 번만 데이터를 가져오는 메소드이면 뭐 하나
getDocs를 무한대로 호출하는데 ㅋ 미쳤군
문제 해결
dependency를 비워줌으로써 이 지긋지긋한 문제가 단번에 해결됐다
useEffect(() => {
fetchTweets();
}, []);
왜 dependency를 비워두지 않고 값을 넣어줬지? 과거의 내 생각을 나도 완전히 알 수는 없지만
실시간으로 데이터를 불러와야 한다고 순간적으로 착각하거나
dependency를 비웠을 때 출력되는 React Hook useEffect has a missing dependency 경고에 잘못 대처한 게 아닐까 싶다.
추가적으로 언급하자면 이 ESlint 경고는
쉽게 말하자면 fetchTweets 함수를 dependency로 선언하거나 useEffect 내부로 이사시키라는 내용이다.
React Hook useEffect has a missing dependency: 'fetchTweets'. Either include it or remove the dependency array. - eslintreact-hooks/exhaustive-deps
그렇다고 fetchTweets 함수를 dependency로 넣으면? 또 데이터를 무한으로 가져오는 상황이 벌어질 수 있지 않을까? ESLint도 이 부분을 바로 경고한다
경고에 대해 조금 더 생각해 보자면
fetchTweets 함수에서는 따로 선언된 user 변수(auth.currentUser: 현재 사용자)를 가져와 사용하고 있는데
'아무래도 user가 변화하면 fetchTweets도 다시 호출해줘야 하지 않겠니? user를 dependency로 넣어' 하고 권장해 주는 것이다.
똑똑한 ESLint가 모든 외부 변수나 함수를 의존성 배열을 추가하라고 경고해 주는 셈.
이는 user를 fetchTweets 내부에서 선언해 주거나, useEffect 내부에서 fetchTweets를 선언해 줌으로써 해결할 수 있다.
export default function UserTimeline() {
const [tweets, setTweets] = useState<ITweet[]>([]);
useEffect(() => {
const fetchTweets = async () => { // << useEffect 내부에서 선언
const user = auth.currentUser; // << fetchTweets 내부에서 선언
/* .... */
};
fetchTweets();
}, []);
/* .... */
해결 완료 모니터링
개선 작업이 반영된 개발 환경과 무한 데이터 콜링 지옥인 배포 환경을 번갈아 실행시켜 보면서
파이어베이스 사용량 변화를 확인했다.
확실히 달라진 상황을 확인!!
dev 브랜치에 작업 내용을 반영하고 main 브랜치에도 머지시켜 배포해 2차 모니터링까지 진행했다.
최종적으로 분당 1,000-2,000회에서 분당 30-100회로 엄청난 개선이 됐다.
완전하게 문제를 해결했음을 확인 👏
작업 내용에 대한 부연 설명 작성해 주면서 이슈를 공식적으로 종료시켰다.
(+) 12/28 firestore 사용량 그래프
작업에 차질 없이 적정량을 사용하고 있어 앞으로도 문제없을 듯하다.
참고
'프로젝트 > ZWITTER' 카테고리의 다른 글
[프로젝트][Zwitter] 기획 및 와이어프레임 만들기 (0) | 2023.12.02 |
---|