DYO 공부하는 블로그
[MusicMate] 프로젝트에서 학습한 내용 정리 (1) 본문
1. 테이블 관리 전략
- 테이블이 분리된 상황에서 조회하기
DB를 3NF를 목적으로 설계했었다. 이럴 때 문제는 이행적 함수를 모두 제거하므로 테이블이 아래와 같은 형태가 된다.

장르 코드는 시맨틱한 정보가 아니다. 따라서 user_genres 테이블만 조회해서는 유저의 선호 장르 정보를 찾을 수 없다. 따라서 유저 장르와 연결된 장르의 이름을 찾기 위해서는 유저 장르 테이블과 장르 정보 테이블을 join한 view로 조회해야만 한다.
당연한 내용이지만 3NF로 구성된 DB를 조회하는 게 처음이라 처음엔 좀 헤맸다. 하지만 이 VIEW를 만능열쇠처럼 유저 선호 장르를 조회할 때마다 사용한다면 의미가 없다는 것도 기억해야겠다.
- 조회는 View 또는 RPC로, 삽입, 삭제, 수정은 테이블로
supabase에서는 클라이언트에서 join으로 요청한 항목에 대해서는 AND 또는 OR 연산이 불가능하다. 따라서 조회는 View 또는 RPC를 이용해야만 했다. 삽입, 삭제, 수정은 테이블의 RLS에 따라 제어되어야만 하므로(아무나 글을 올리거나, 다른 사람이 쓴 글을 삭제할 수 있으면 안 됨.) 테이블을 통해 처리한다.

- 최적화는 백엔드만 하는 게 아니다.

백엔드에서는 서버의 조회, 삽입, 삭제 등의 동작에 대해서 탐색 방법, 대체 방법, 정렬 방법 등을 고민하면서 최적화한다. 그런데 클라이언트단에서 요청에 대해 최적화를 하지 않는다면 요청량이 폭증하기 때문에 유저 인증 정보와 같이 한번만 요청해도 토큰 시간동안은 상관 없는 내용은 한번 요청하고 그 데이터를 계속 이용하는 게 좋다고 느꼈다.

무료 플랜 사이즈가 작은데 이미지와 오디오 파일을 이용한 프로젝트인 것도 문제였다. 서버에 요청한 파일들에 대해 캐싱을 무시하고 그대로 뿌려 줬더니 데이터 요청량이 많았었는데, 브라우저의 캐싱에 의존하게 수정한 뒤에 데이터 요청량이 많이 줄어든 모습을 확인할 수 있었다.
- 요청을 줄여보자.
// 유저아바타프리뷰 url 가져오기
const getPreviewImage = async (
feed: Tables<"get_feeds_with_user_and_likes">
): Promise<string | undefined> => {
if (!feed.author_profile_url) return;
const previewUrl = await getAvatarUrlPreview(feed.author_profile_url);
if (!previewUrl) return;
return previewUrl;
};
const updatedFeeds = await Promise.all(
afterFeedData.map(async (feed) => {
const previewUrl = await getPreviewImage(feed);
return { ...feed, preview_url: previewUrl };
})
위 코드는 프로필 사진의 public url 정보를 매 게시글마다 서버에 요청하면서 받아오고 있다.
하지만 이미 bucket의 주소와 파일의 이름을 모두 알고 있으므로 현재 유저의 정보와 조합하여 서버에 요청하도록 변경했다.
export const profileBucketUrl = `${import.meta.env.VITE_SUPABASE_URL}/storage/v1/object/public/user-avatar/`
const getPreviewImage = async (
feed: Tables<"get_feeds_with_user_and_likes">
): Promise<string | undefined> => {
if (!feed.author_profile_url) return;
return `${profileBucketUrl}/${feed.author_profile_url}`
};
위 코드의 변경으로 인해 게시글당 한 번씩 요청하던 url 정보 요청을 줄일 수 있었다.
2. Controlled Component를 항상 적용해야 하는가?
input range의 value를 오디오 플레이어와 같은 형태의 주기적으로 갱신이 필요한 항목에 대해서 State를 이용해 Controlled Component를 제어할 경우 갱신 주기만큼 컴포넌트를 리렌더링하게 된다. 이와 같은 경우에는 사용자가 input 요소를 Click한 상태를 추적하는 State를 하나 추가하고, 주기적 갱신은 ref를 이용해 갱신해주는 것으로 렌더링 트리거 동작을 줄일 수 있다.
사실 input 컴포넌트를 분리한 뒤 State를 따로 두면 해당 컴포넌트만 재렌더링되어야 하는데, 예상치 못한 사이드이펙트로 레이아웃 요소들이 리렌더링되는 문제가 있어 이런 방식으로 리팩터링할 필요성이 있었다.
이번 회고 요약
- 3NF로 분리된 DB에서 시맨틱 정보가 없을 땐, View 조인으로 조회하고, 삽입/삭제는 테이블로 처리해야 한다.
- Supabase는 클라이언트에서 조인된 데이터에 조건 필터링이 불가능하므로, View나 RPC 전략이 필수다.
- 클라이언트도 최적화 주체이며, 반복 요청은 캐싱이나 ref 등을 활용해 최소화해야 성능 문제가 생기지 않는다.
- 항상 Controlled Component일 이유는 없다. 리렌더링 사이드 이펙트를 방지해보자.
'Project > MusicMate' 카테고리의 다른 글
| [MusicMate] 프로젝트에서 학습한 내용 정리 (3) (5) | 2025.08.17 |
|---|---|
| [MusicMate] 프로젝트에서 학습한 내용 정리 (2) (7) | 2025.08.09 |
| [MusicMate] 팀 프로젝트 - 프로젝트 시작 (3) | 2025.07.24 |