[인프런 워밍업 클럽 3기] 풀스택 스터디 3주차 미션 회고 발자국
학습 내용 요약인프런 워밍업 클럽 3기 풀스택 스터디 3주차에는 Supabase Database를 활용하여 페이지네이션을 다루는 방법을 학습하였습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다. Supabase Client Query Builderlet { data: movie, error } = await supabase .from('movie') // 'movie' 테이블에서 데이터를 가져옴 .select("*") // 모든 컬럼을 선택 // 필터 조건 (WHERE 절과 유사) .eq('column', 'Equal to') // column이 'Equal to' 값과 같은 경우 .gt('column', 'Greater than') // column이 지정 값보다 큰 경우 (>) .lt('column', 'Less than') // column이 지정 값보다 작은 경우 (<) .gte('column', 'Greater than or equal to') // column이 지정 값보다 크거나 같은 경우 (≥) .lte('column', 'Less than or equal to') // column이 지정 값보다 작거나 같은 경우 (≤) .like('column', '%CaseSensitive%') // column이 특정 패턴과 일치하는 경우 (대소문자 구분, LIKE '%...%') .ilike('column', '%CaseInsensitive%') // column이 특정 패턴과 일치하는 경우 (대소문자 구분 없음, ILIKE '%...%') .is('column', null) // column 값이 NULL인 경우 .in('column', ['Array', 'Values']) // column이 지정된 배열 값들 중 하나와 일치하는 경우 (IN 연산자) .neq('column', 'Not equal to') // column이 지정된 값과 다른 경우 (!=) // 배열 관련 필터 .contains('array_column', ['array', 'contains']) // array_column이 주어진 배열 요소를 모두 포함하는 경우 .containedBy('array_column', ['contained', 'by']) // array_column이 지정된 배열에 완전히 포함되는 경우 // 논리 연산자 .not('column', 'like', 'Negate filter') // column이 'like' 조건을 만족하지 않는 경우 (NOT) .or('some_column.eq.Some value, other_column.eq.Other value') // OR 연산자: some_column이 'Some value'이거나 other_column이 'Other value'인 경우 Supabase에서 Text와 Varchar 이해하기요약두 유형 모두 문자열을 저장하는 목적으로 사용됨저장 방식과 성능에 대한 차이가 있지만, Supabase는 단순성을 강조하므로, 특별한 이유가 없다면 기본적으로 text를 사용하는 것이 권장됨 Text긴 문자열을 저장하는 데 사용됨문자 개수에 대한 제한이 없으며, 길이를 예측하기 어려운 문자열에 적합함VarcharVariable Character Length(가변 길이 문자)의 약어최대 길이를 설정할 수 있음 → 데이터 일관성을 유지하는 데 유용하며, 특정 쿼리에서 성능이 약간 향상될 수 있음 페이지네이션 구현 방식: Offset vs CursorOffset Based Pagination 동작 방식OFFSET과 LIMIT을 사용해 특정 범위의 데이터를 가져옴장점특정 페이지로 바로 이동 가능 (예: 1페이지, 5페이지 등)직관적이고 구현이 간단함단점데이터가 많아질수록 OFFSET 성능 저하 (큰 OFFSET 값일수록 느려짐)데이터가 변경되면 순서가 달라질 수 있어 불안정함Cursor Based Pagination동작 방식마지막 항목의 특정 필드(예: created_at 또는 id)를 커서로 사용해 이후 데이터를 가져옴 장점성능이 우수함 (특히 큰 데이터셋에서 OFFSET 사용 없이 빠르게 조회 가능).데이터가 변경되더라도 안정적인 페이지네이션이 가능함.단점특정 페이지로 바로 이동이 어렵고, 이전 페이지로 돌아가는 것이 복잡할 수 있음.구현이 상대적으로 복잡함. Netflix 클론 미션 회고풀스택 스터디 3주차 미션은 강의에서 진행하는 Next.js와 Supabase Database를 활용한 Netflix 클론 앱에 찜 기능을 추가하는 것이었습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다. 미션 수행 내용아래는 제가 수행한 미션에 대한 내용을 정리해보았습니다. 영화 목록 조회 기능찜 리스트 조회 기능키워드 검색 기능무한 스크롤 지원영화 상세 정보 조회 기능동적 메타데이터 지원SSR 지원영화 찜하기 기능낙관적 업데이트 지원 사용 기술프레임워크: Next.js v15, React v19데이터베이스: Supabase서버 상태관리: Tanstack Query v5클라이언트 상태관리: Zustand v5스타일 프레임워크: TailWindCSS v3, Mantine v7, Tabler Icons모노레포: Turbo Repo패키지 매니저: pnpm 페이지네이션강의에서는 옵셋 기반으로 페이지네이션을 구현하였지만, 저는 커서 기반 페이지네이션이 데이터 추가 또는 삭제 시 페이지 별 인덱스가 꼬여 다른 페이지에 같은 데이터가 존재하거나 데이터를 건너뛸 수 있는 문제가 없기에 무한스크롤 기능에 조금 더 적합하다고 판단하여 커서 기반으로 페이지네이션을 구현하였습니다.export interface SearchMoviesParams { keyword?: string cursor?: number | null size?: number like?: boolean } export interface SearchMovies { (params: SearchMoviesParams): Promise<{ data: Movie[] nextCursor: number | null first: boolean last: boolean }> } export const searchMovies: SearchMovies = async ({ cursor = null, keyword = '', like = false, size = 12, }: SearchMoviesParams) => { const client = await createServerSupabaseClient() const first = !cursor let query = client.from('movie').select('*').order('id', { ascending: true }) if (keyword) { query = query.ilike('title', `%${keyword}%`) } if (like === true) { query = query.eq('is_like', true) } if (cursor) { query = query.gt('id', cursor) } const { data, error } = await query.limit(size + 1) if (error) { console.log(error) return { data: [], nextCursor: null, first, last: true, error } } const hasNextPage = data.length > size const nextCursor = hasNextPage ? (data[size - 1]?.id ?? null) : null return { data: ( data?.map((movie) => ({ id: movie.id, title: movie.title, imageURL: movie.image_url, overview: movie.overview, popularity: movie.popularity, releaseDate: movie.release_date, voteAverage: movie.vote_average, isLike: movie.is_like, })) ?? [] ).slice(0, size), nextCursor, first, last: !hasNextPage, } } 찜 기능 테이블 스키마아래는 3주차 미션인 찜 기능을 구현할 때 작성한 movie 테이블 스키마입니다.CREATE TABLE movie ( id SERIAL PRIMARY KEY, image_url TEXT NOT NULL, title TEXT NOT NULL, overview TEXT NOT NULL, vote_average FLOAT8 NOT NULL, popularity FLOAT8 NOT NULL, release_date DATE NOT NULL, is_like BOOLEAN NOT NULL DEFAULT FALSE );해당 프로젝트에서는 별도로 회원 관리를 하지 않기 때문에 간단하게 컬럼을 하나 추가하여 구현하였지만, 만약 회원이 존재하는 상황이라면 테이블을 따로 분리한 후 회원 id와 영화 id를 받아와서 왜래키(FK)로 관리하는 방식도 괜찮았을 것 같습니다. 후기이번 주차에는 백엔드의 대표적인 작업 중 하나인 페이지네이션을 학습해 볼 수 있었습니다. 강의와는 다르게 Supabase를 사용한 커서 기반 페이지네이션을 구현해 보았는데, 공식 문서를 읽는 것이 쉽지 않았던 것 같습니다. 이전 주차들에서도 동일하게 Supabase 클라이언트에서 제공하는 쿼리 빌더 메서드들이 무슨 역할을 하는지 정리 해야겠다고 생각했는데 이번 주차에 드디어 하게 되었네요. 이제 다음 주차 미션인 인스타그램 클론까지 학습하면 간단한 MVP는 혼자서 구현해 볼 수 있을 것 같아서 기대됩니다. 긴글 읽어주셔서 감사합니다. ☺