-
[WizSched] Supabase Table에 데이터를 넣어보자Front-End/Project 2025. 2. 14. 21:30
🍀목차
Table 만들기
Form 컴포넌트, Zod 라이브러리
테스트Table 만들기
Project > Table Editor Enable Row Level Security를 체크한다. - Enable Row Level Security(RLS) : Database 보안 기능 중 하나, 활성화하면 Postgres Policy를 작성하여 해당 테이블에 대한 접근을 제한할 수 있다(예를 들어, 로그인한 회원만 데이터 접근이 가능하도록 할 수 있다).
RLS가 어떤 기능인지 와닿지 않아 RLS를 체크한 테이블(
YES_RLS
)과 체크하지 않은 테이블(NOT_RLS
)을 만들어 테스트해 봤다.NOT_RLS는 좌물쇠 아이콘이 풀려있다. YES_RLS는 기본으로 권장하는 옵션이다. RLS를 체크한 테이블은 기본적으로 Policy(정책)이 설정되어 있어야 데이터 조회가 된다. 정책을 설정하지 않는다면 조회가 불가능하다.
YES_RLS에 policy가 없는 경우 정책을 설정해보자. policy 설정 로그인한 사용자만 SELECT 접근이 가능하도록 설정해 봤다.
policy가 등록되었다. 권한이 없을 경우 데이터가 조회되지 않는다(YES_RLS). 로그인 시에는 조회된다(YES_RLS). 모든 Supabase 프로젝트의 데이터베이스는 PostgreSQL을 사용한다. 관계형 데이터베이스이며, 다양한 데이터 유형과 구조화된 데이터 유형, 복잡한 데이터 유형을 모두 관리할 수 있는 유연성을 갖고 있다.
테이블을 만들면서 낯설었던 PostgreSQL 데이터 타입에 대해 정리했다.
- UUID(Universally Unique Identifier) : 문자열 형태로 저장된다(TypeScript에서 string으로 정의). 랜덤키이며 추측해서 요청하는 것이 불가능에 가깝다. 해당 특성으로 외부에 노출되는 용도의 API용 ID는 Alternate Key(보조키)로 uuid로 채워진 컬럼을 사용한다. Primary Key(기본키)로는 잘 사용하지 않는데, 의미적으로 알아보기 힘들며 정렬도 불가, 용량도 크기 때문이다.
- VARCHAR vs TEXT
- 공통점 : 둘 다 String 데이터 타입이다.
- TEXT는 최대 길이를 지정하지 않고 저장하는 문자열 타입이다.
- VARCHAR는 길이 제한이 가능하다.
- 보통 VARCHAR가 더 유리한 성능을 가지지만, PostgreSQL은 비슷한 성능을 가진다고 한다.
- Numeric Types
- smallInt(int2) : 2 byte, -32768 to +32767
- integer(int4) : 4 byte, -2147483648 to +2147483647
- bigInt(int8) : 8 byte, -9223372036854775808 to +9223372036854775807
- JSON vs JSONB
- JSON : 들어온 값을 그대로 저장한다. 인덱싱이 불가능하다.
- JSONB(Binary) : 정제(공백 제거 등)를 거친다, 인덱싱이 가능하다.
Form 컴포넌트, Zod 라이브러리
Form 컴포넌트 구현에는 제어, 비제어 컴포넌트 사이에서 고민을 했다.
- Uncontrolled Component(비제어 컴포넌트)
- DOM 자체에서 폼 데이터가 다뤄진다.
- 값을 가져오려면 ref를 사용한다.
- submit 할 때, 폼 값을 얻어온다.
- 실시간으로 데이터 유효성을 검사하거나, 값에 의해 트리거 되는 행위(disabled 버튼이 된다던가)를 구현하기 어렵다.
- Controlled Component(제어 컴포넌트)
- React 컴포넌트에서 폼 데이터가 다뤄진다.
- 사용자의 입력을 기반으로 React state와 결합하여 신뢰 가능한 단일 출처로 만든다.
- 값은 React state와 동기화된다.
- 사용자가 입력하면, 리렌더링이 발생한다.
결과적으로는 비제어 컴포넌트를 선택했다.
React에서 제어 컴포넌트를 권장하기도 하고, 관련 라이브러리(react-hook-form)도 있지만 선택 이유는 아래와 같다.
1️⃣ 실시간 동기화가 필요하지 않다. 많은 state를 관리하고 싶지 않다.
2️⃣ 유효성 검증은 zod 라이브러리의 도움을 받고자 했다.
3️⃣ Formdata를 활용하는 것에 대한 큰 단점을 못 느꼈다.
몇 년 전 만들었던 form
(부끄럽지만) 위 코드는 몇 년 전, React를 처음 사용했었던 프로젝트 코드이다. 매우 극단적인 예시지만 위와 같은 방식으로 Form을 관리하고 싶지 않은 마음이 컸다. 또, 웬만하면 컴포넌트 구현에 라이브러리의 도움을 받고 싶지 않았다.완성된 Form 컴포넌트 구조이다.
const TaskForm = () => { const handleSubmit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const data = Object.fromEntries(formData.entries()); // 생략 ... }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="summary">제목: </label> <input type="text" name="summary" id="summary" placeholder="제목" /> </div> <div> <label htmlFor="description">설명: </label> <input type="text" name="description" id="description" placeholder="설명" /> </div> {/* 생략.. */} <Button variant="outlined" color="success" type="submit"> 만들기 </Button> </form> ); }; export default TaskForm;
유효성은 zod 라이브러리의 도움을 받았다. 타입스크립트와 시너지도 좋고, 여러 조건을 동시에 처리하기 편해 보였다. 주위에서 추천도 많이 받아서, 개인적으로 사용해보고 싶었던 라이브러리였다.
Yup, Joi 같은 다른 라이브러리와의 비교는 공식 문서에 안내되어 있다.
// 설치 yarn add zod
import { z } from 'zod'; export const TaskSchema = z .object({ summary: z.string().min(1).max(200), description: z.string().nullish(), date: z.string().date(), start: z.string().time().nullish(), end: z.string().time().nullish(), syncGoogleCalendar: z.boolean().default(false), }) .strict(); export type Task = z.infer<typeof TaskSchema>;
Schema는 이렇게 정의했다.
.strict()
을 사용하면 Schema에 정의된 필드가 아닐 경우 에러를 던져주는데(공식 문서), 중간에 google -> syncGoogleCalendar로 변수명을 바꿨을 때 유용했다.에러 메시지도 변경해 줬다(공식 문서).
export const FIELD_NAMES: Record<string, string> = { summary: '제목', description: '설명', date: '날짜', start: '시작 시간', end: '종료 시간', } as const;
export const taskSchemaErrorMap: z.ZodErrorMap = (issue, ctx) => { const field = issue.path[0]; if (field in FIELD_NAMES) { const fieldName = FIELD_NAMES[field]; switch (issue.code) { case z.ZodIssueCode.too_small: return { message: ERROR_MESSAGES.too_small(fieldName) }; case z.ZodIssueCode.too_big: return { message: ERROR_MESSAGES.too_big(fieldName) }; case z.ZodIssueCode.invalid_type: return { message: ERROR_MESSAGES.invalid_type(fieldName) }; case z.ZodIssueCode.invalid_date: return { message: ERROR_MESSAGES.invalid_date(fieldName) }; case z.ZodIssueCode.invalid_string: return { message: ERROR_MESSAGES.invalid_string(fieldName) }; } } return { message: ctx.defaultError }; };
만들어준 Custom Error Map을 Schema에 연결해 주면 된다.
유효성 검증을 통과하면 Supabase의 Table에 insert, 실패 시 에러메시지를 안내해 주었다.
API Docs TABLES AND VIEWS -> 만든 Table 확인 Supabase API Docs에서 Table에 대한 여러 기능을 코드로 편하게 참고할 수 있다.
테스트
ZodError 형식 아직 Toast 컴포넌트를 구현하지 않아 에러 메시지는 임시로 alert에 띄워주었다.
alert( Object.values(result.error.format()) .flatMap((error) => { if (Array.isArray(error)) { return error; } return error._errors; }) .join('\n'), );
검증 실패 검증 성공 데이터 검증 성공 테이블에 데이터가 추가되었다. 참고자료
https://ko.legacy.reactjs.org/docs/forms.html#controlled-components
https://zod.dev/?id=table-of-contents