[개인 프로젝트 ShareLife] SNS 만들기 2차 최적화 및 리펙토링 및 에러 수정

규갓 God Gyu·2024년 10월 29일
0

프로젝트

목록 보기
70/81

수파베이스 email provider 업데이트로 인한 회원가입 이메일 인증 제한 + resend 이용한 SMTP 세팅 오류

수파베이스 프로젝트를 만든지 시간이 좀 지나니까

이메일 인증이 현재 프로젝트의 조직 멤버로만 제한되어 있다는 것을 의미

근데 애초에 이메일 인증 없이 회원가입 진행하게 처리해놨었는데, 왜 이런 조치가 생겼는지는 의문

그래서 혹시 회원가입에 문제가 있나 테스트해봤는데 진짜로 안되었다.

즉, 초기 프로젝트에선 이메일 인증 없이도 문제가 없었지만,

조직의 정책 변경으로 인해, 특정 조직 내에서 운영되고 있어서 이러한 문제가 야기되었다해서 공식문서를 참고해보니

정리하자면 9월 20일부터 업데이트 된 따끈따끈한 내용인데 정리하자면 무료 플랜 사용자들이 안정적이고 안전하게 계속 서비스 이용할 수 있도록 하기 위한 업데이트.

  • 이메일 템플릿 사용자 정의 제한
    • 커스텀 SMTP 설정하지 않은 프로젝트는 더 이상 이메일 템플릿을 사용자 정의할 수 없음. 기존 사용자 정의 템플릿은 기본 템플릿으로 돌아감. 그러나 커스텀 SMTP를 사용하는 경우 여전히 템플릿을 사용자 정의할 수 있음.
  • 이메일 발송 제한
    • 기본 이메일 제공자를 사용하면 프로젝트 조직 내 이메일 주소로만 이메일 발송할 수 있음. 인증 이메일은 해당 프로젝트의 구성원에게만 발송.
  • 커스텀 SMTP 설정 권장
    • 프로젝트 인증 이메일 유연하게 관리하고 싶으면, 커스텀 SMTP 제공자를 사용하는 것이 권장.
  • 비밀번호 재설정 흐름 영향
    • 이메일 인증 꺼도 비밀번호 재설정 등의 이메일은 계속 발송되며, 기존 프로젝트 구성원에게만 발송됨. 이 외의 사용자는 비밀번호 재설정 기능이 사실상 중단됨

그렇다면 앞으로 기존에 저장된 조직원이 아닌 이상 새로 회원가입하려면 커스텀 SMTP설정을 하면 되는 것 같은데?

SMTP Settings란?
수파베이스와 같은 애플리케이션에서 이메일을 발송하기 위해 사용되는 메커니즘
기본 이메일 서비스 대신 사용자 자신의 SMTP 서버를 사용하여 이메일 전송할 수 있도록 해주는 설정

이메일 발송을 위한 공급자에 가입을 해줘야함.
그리고 무료 버전은 시간당 최대 3개의 이메일만 전송할 수 있음 -> 회원가입 시간당 3개
그래서 커스텀 SMTP을 통해 이메일 시간당 전송 수를 설정할 수 있게됨

그 많은 공급자 중에 Resend SMTP를 사용해보겠다.
근데 정확히 어떻게 api를 세팅하라고 안나와서 그냥 일단 진행해보겠다;;

키를 추가해주고,


도메인 인증을 추가해줬는데, 이게 이제 회원가입할 사람에게 보내는 도메인이라고 생각하면 될 것 같다. 처음해보는거라 맞는지 모르겠네;;

근데 역시나 수파베이스에서 세팅하려니까 막혀서, 기존에 가비아에서 구입한 다른 프로젝트 구현할때 사용한 도메인을 수정해서 사용해보겠다.

기존 booker프로젝트할때 사용한 도메인에 TXT, MX 추가해준 모습 => resend에서 받은 값을 그대로 넣어줬다.


SMTP 세팅 resend와 병합하여 완료 과연 회원가입은 될것인가..!


이제 정상적으로 회원가입은 진행되었고 역시나 로그인은 안되었다.

확인해보니, DNS관련되어서 가비아에 추가만 했지 검사를 안해서 그런건데

이걸 먼저 기다렸어야 했다. 몇번 해봤어도 항상 까먹는 절차 ㅋㅋ ㅠㅠ

너무 오래 걸려서 그동안 다른걸 진행하도록 하겠다.

지금 한번 domain verifying에 대해서 최소 몇시간이 걸리는데 3번이나 실패하였다. MX, TXT 값을 가비아의 DNS 레코드에 잘 입력했음에도 안되니 문의메일은 보내놨고, 다른 사이트로 SMTP를 진행해봐야겠다 ㅠㅠ

SMTP gmail로 세팅 => SMTP 비활성화로 해결

다시 검색하다보니 gmail에서도 SMTP 세팅을 할 수가 있어서
gmail 2단계 인증 세팅 - 앱 비밀번호 생성 - SMTP 비밀번호를 받은 후,

개인 이메일로 전송되겠지만 그래도 gmail버전으로 재도전을 해보고 있다.

근데 굳이 없어도 된다해서 그냥 다시 비활성화하고 해보니까?? 왜 되냐??
회원가입이? 안되었자나 ㅡㅡ 하............

이 망할 에러만 아니였어도..... ㅠㅠ
그냥 잠깐 수파베이스쪽 에러였다고 생각하자.. 몇일 날리긴했지만..

zod 비밀번호 유효성 검사 강화

기존에 수파베이스 자체에서 비밀번호에 대한 유효성 검사를 강화하다보니, 클라이언트에서는 비밀번호 유효성검사를 딥하게 하지 않은걸 몰랐는데, 이번에 코드를 보다보니 알게되었다.
min 8자 이후 추가적인 유효성 검사 슈웃~!

nextauth email provider 추가 => 제거(갑자기 기존 회원가입 잘 진행됨;;)

아직 이메일 인증이 되는지 안되는지는 모르지만 그래도 클라이언트 측에서 nextauth로 인증 인가를 관리하는 만큼 email provider도 미리 추가해줘보자

이후

이 값을 넣어주려고 다시 resend를 들어가니 api key 복사하는 곳이 사라졌따. 그래서 아무리 찾아도 없어서 다시 api 세팅을 해주었다.

확인해보니 integration으로 수파베이스와 통합을 하니까 api키없이도 연동이 되어서 api키가 없던 문제가 발생한 것인데, 따로 resend에서 자체 api키를 세팅해주면 된다.


그렇게 API키를 자체적으로 만들고, 수파베이스의 password와 .env파일 안에 PASSWORD안에 넣어줬다. 이게 잘 될지 안될지는 실제로 테스트를 해봐야하는데 아쉽네 ㅠㅠ

일단 .env파일은 추가했고,

nextauth provider에 EmailProvider를 추가해주는데 여기서 nodemailer 라이브러리를 다운받아서 적용하라고 되어있었다.

nodemailer란
nextAuth가 이메일 인증을 위한 SMTP서버와의 통신을 관리해주기 위함

  • 이메일 전송 : 이메일 프로바이더 사용해 이메일 보내려면 SMTP서버와의 연결이 필요하면 nodemailer가 처리함
  • 간편한 설정 : nodemailer는 이메일 전송을 위한 다양한 기능과 설정을 제공하므로, 이메일 전송 간편하게 할 수 있게 도와줌

근데 공식문서에 있는 추가를 하니 모듈이 불러와지지 않아서

pnpm add -D @types/nodemailer
이 라이브러리로 추가하니 정상적으로 모듈이 받아졌다 굳~~

import { supabase } from "@/lib/supabase"
import { AuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import EmailProvider from "next-auth/providers/email"
import nodemailer from "nodemailer"

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "text", placeholder: "you@example.com" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials) {
          console.error("Credentials are missing.")
          return null
        }

        const { email, password } = credentials

        const { data, error } = await supabase.auth.signInWithPassword({
          email,
          password,
        })

        if (error) {
          console.error("Supabase sign-in error:", error)
          throw new Error(error.message) // 사용자에게 에러 메시지 전달
        }

        if (!data.user) {
          console.error("No user found.")
          return null
        }

        return { id: data.user.id, email: data.user.email }
      },
    }),
    EmailProvider({
      server: {
        host: process.env.EMAIL_SERVER_HOST,
        port: process.env.EMAIL_SERVER_PORT,
        auth: {
          user: process.env.EMAIL_SERVER_USER,
          pass: process.env.EMAIL_SERVER_PASSWORD,
        },
      },
      from: process.env.EMAIL_FROM,
    }),
  ],
  pages: {
    signIn: "/login",
  },
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.email = user.email
      }
      return token
    },
    async session({ session, token }) {
      if (token) {
        session.user = {
          id: token.id as string,
          email: token.email as string,
        }
      }
      return session
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
}

근데 실질적으로 nodemailer는 사용은안하는데 import하는것만으로 작동을 한다는게 좀 신기한듯?

근데 결과적으로 SMTP는 백엔드에서 처리하는 로직을 구현하는게 더 의의가 있기도하고 없이도 다시 잘 회원가입이 되어서 다시 제거하였다..

시간 버리긴했지만 또 거기안에서 배울 점은 있었따.

zod 리펙토링 & react-hook-form과 연동 - safeParse 제거

zod의 스키마는 특수문자와 대소문자 숫자 등 추가적으로 유효성 검사를 강화하면서 react-hook-form과 연동하는 코드를 확인하다보니 zodResolver옵션을 통해 zod와 연결을 했었었지만, 실제 로그인 시키는 함수 안에도 safeParse 메서드를 통해 zod와 연결하고, react-hook-form과 zod를 연동할때 zodResolver를 통해 총 2번이나 zod와 폼을 연결하고 있었는걸 발견했다.

하루만에 다 구현했으면 이런건 인지했을텐데 몇일 동안 여러개 하다보니 이렇게 문제를 발견했고,

zodsafeParse메서드는 데이터를 받아 직접 유효성 검사를 수행, 결과를 리턴함. 성공 여부와 함께 데이터를 검증할 때 사용하고, success:true와 파싱된 데이터, 실패 시 success:false와 에러 메시지를 반환하는데 사용

그러나 zod와 연동을 하지 않고 그냥 자체적으로 유효성 검사를 수행. 사실상 검사 수행하는데 있어서는 문제가 전혀 없음.

zodzodResolver는 react-hook-form과 zod를 자동으로 연동해주는게 큰 차이점. 그래서 폼 제출하면 zod 스키마에 맞춰 유효성 검사를 진행함. react-hook-form의 resolver옵션에 넣어주면 됨.

그리고 그렇게 연동을 하면 필드의 에러를 formState.errors에서 관리할 수 있어서 추가적인 검증 로직이 필요하지 않음.

그래서 safeParse가 아닌 zodResolver로 연동하면서 safeParse를 제거하니 그래도 잘 작동함.(스키마 내용이 잘 유효성 검사를 진행해줌)

근데 그러다가 error메세지를 나타내려면 formState.errors를 사용하기 위해 실제 폼도 리펙토링을 진행했는데, 기존에는 useForm만 사용해서 form을 선언해줬고, 폼 상태를 여러 컴포넌트 간에 공유할 때는 FormProvider로 묶어서 관리해줘야한다고 알게되었다.
즉, 상태를 전역적으로 제공해서, prop drilling없이 하위 컴포넌트에서 useForm의 상태에 쉽게 접근할 수 있음.

근데 이 폼을 사용하는 공간은 로그인/회원가입페이지만 존재해서, 그냥 useForm만 사용해도 별 문제는 없지만, 회원가입에서 입력한 이메일, 비밀번호 값을 로그인페이지로 이동하면서 바로 넘겨줘서 로그인 버튼만 누르면 빠르게 로그인 할 수 있게 진행할 수도? 혹은 바로 로그인까지 진행해도 괜찮지 않을까? 라는 생각이 들었다.

그리고 실제 폼 작성하는 곳은 로그인/회원가입/게시글/댓글 4군데기 때문에 전역 상태 관리 및 재사용성을 염두에 두고 FormProvider로 사용하기로 생각해보았다.

그리고 앞서 말한 데이터 값을 넘겨주려면 전역 상태관리로 겸사 겸사 zustand를 사용할 예정(이게 맞는진 모르겠지만)

근데 여기서 의문점인건 FormProvider의 폼 상태관리랑 회원가입에서 받은 정보 로그인쪽으로 넘겨주는 데이터 전역상태관리하는거랑 무슨차이지? 싶었다.
(아직 리엑트 훅 폼까지 리펙토링할진 미정)

react-hook-form 리펙토링(mode 추가)

회원가입 폼 - 이름/이메일/비밀번호/주소 등 여러 입력 필드를 받아야함.
하나의 컴포넌트 안에 모드 작성하면 코드가 너무 길고, 유지 보수가 어려워짐.
여러 하위 컴포넌트로 나누면, 각 필드를 별도의 컴포넌트로 나누어 관리할 수 있음.
그리고 하위 컴포넌트가 폼의 상태를 공유하고, 입력 값, 에러 상태 같은 폼 관련 상태를 쉽게 관리할 수 있음

근데 뭐가 되었든 리엑트 훅폼에 대해 좀 더 파고들어서 근본적인 사용 이유를 더 자세히 알고 싶었는데,

비제어 컴포넌트
React에서 폼 데이터를 직접적으로 상태로 관리하지 않는 방식
ex - input, select의 값이 DOM 자체에 의해 관리되며 react는 상태를 추적하지 않음
대신 필요할 때만 값을 가져옴.
ref를 사용해 특정 시점에 DOM에서 값을 추출하는 방식
그래서 상태가 변경될 때마다 컴포넌트가 리렌더링 되는 현상을 방지하여 성능이 향상될 수 있음.

리엑트훅폼은 이러한 비제어 컴포넌트 방식을 채택해서 폼 상태를 더욱 효율적으로 관리할 수 있게 해주는데, 각 입력 필드의 값(value)를 DOM에서 직접 관리하며, 상태는 필요할 때만 업데이트 됨.
대규모 폼이나 성능이 중요한 폼에서 유리한 이점을 제공.

그러나 실질적으로 control옵션을 통에 제어 컴포넌트 즉, 리엑트에서 상태 추적하고 상태 변경되면 리렌더링되는 상태로 리엑트훅폼을 사용했는데,
그 이유는 상태가 변화하는걸 추적해서 유효성 검사를 진행해줘야 하기 때문이다. 비제어 컴포넌트 방식에선 입력 필드의 값을 직접적으로 react상태로 관리하지 않아 즉시 입력된 값을 가져오지 못함. 그리고 값을 가져오려면 ref를 사용해야하는데, 입력값이 변경될 때마다 상태를 업데이트하지 않기 때문에, 실시간 유효성 검사 구현하기 어려움.

그리고 실시간 정말 한글자 한글자에 대한 유효성 검사를 위해선 mode를 onChange로 해주면, 한글자마다 zod가 유효성검사를 진행해줄 수 있어서 그 모드를 회원가입에서만 추가해줬다.

  const form = useForm<userType>({
    resolver: zodResolver(RegisterSchema),
    mode: "onChange",
  })

next-auth 정리 및 리펙토링(토큰 만료 시간 설정)

기존 코드부터 이해하고 넘어가보자

JWT
사용자 인증 정보를 안전하게 전달하기 위해 사용하는 JSON 형식의 토큰
클라이언트와 서버 간에 정보를 안전하게 교환하기 위한 방식

  • 헤더 : 토큰의 타입(JWT)와 사용할 알고리즘 지정
  • 페이로드 : 실제 데이터 포함(사용자 ID, 만료 시간 등과 같은 클레임)
  • 서명 : 헤더와 페이로드를 조합하여 비밀 키로 서명

세션
사용자가 애플리케이션에 접속할 때 생성되는 일시적인 상태 정보 저장하는 방법.
사용자가 인증된 후, 서버는 해당 사용자의 정보를 세션에 저장하여 이후 요청 시 이를 참조

  • 서버 측 세션: 서버는 세션 ID를 생성하고, 이를 클라이언트에 쿠키로 전달. 서버는 세션 ID를 키로 사용하여 사용자의 상태를 저장.
    사용자가 요청을 보낼 때마다 쿠키에 포함된 세션 ID를 사용하여 서버에서 세션 정보를 조회
  • JWT 사용: JWT를 사용할 경우, 세션 정보를 클라이언트 측에서 토큰으로 저장. 사용자가 로그인하면 JWT를 생성하고, 클라이언트(로컬 스토리지, 쿠키 같은)에 저장. 이후 요청 시 JWT를 Authorization 헤더에 포함하여 서버로 전달

즉 나는 nextauth.js라이브러리에서 인증 로직을 구현했는데,
session strategy를 jwt로 설정하여 세션을 관리함. 사용자 인증 정보를 토큰으로 저장 후 클라이언트가 요청할 때마다 사용.

callbacks를 사용하여 jwt를 생성하고사용자의 정보를 토큰에 추가함.
세션이 생성될 때 토큰에서 사용자 정보를 세션에 추가함

CredentialsProvider사용해 이메일 비밀번호로 supabase에서 인증 수행

middleware를 사용하여 인증된 사용자만 특정 경로에 접근할 수 있도록 제한

nextauth jwt 토큰 연장 추가

지금까지는 토큰에 대해서 무제한 만료시간없이 사용하고 있었는데, 보안측면에서 위험할 수 있어 일단 만료시간을 추가해주었다.

  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.email = user.email

        //만료 시간 (1시간으로 일단 설정)
        token.exp = Math.floor(Date.now() / 1000) + 60 * 60
      }
      return token
    },
    async session({ session, token }) {
      if (
        typeof token.exp === "number" &&
        token.exp < Math.floor(Date.now() / 1000)
      ) {
        session.user = { id: null, email: null }
      } else if (token) {
        session.user = {
          id: token.id as string,
          email: token.email as string,
        }
      }

      return session
    },
  },

user가 있을때 token에 사용자 ID와 이메일을 저장하고,
JWT 토큰의 만료 시간을 1시간으로 설정함.
그 후 사용자가 세션을 요청할 때 콜백함수가 실행되는데, 페이지를 새로고침하거나 서버 측에서 세션 필요로 할 때 호출
그래서 만료된 토큰이면 사용자 정보를 제거 시킴

기존 토큰 만료시간 설정 안했을때처럼 토큰 시간을 길게 사용하고 싶은데,
만료 시간 설정이 없으면 보안 취약성이 있기 때문에, 리프레시 토큰을 통해 세션 연장을 고려해 보겠다.

리프레시 토큰 장점

  • 보안성
  • 유연성

근데 지금 내가 사용하는 방법은 JWT 기반인데, nextauth가 쿠키를 통해 jwt를 관리하고 있음.

이것저것 조사하다보니 nextAuth 자체 옵션에서 세션을 자동적으로 연장해서 토큰 시간 만료가 필요 없이 세션 관리가 가능하다는 사실을 알게되어
token.exp로 토큰시간 설정하는 옵션을 빼주고 nextAuth의 세션 시간 관리쪽을 추가해주었다.

토큰 만료

  • JWT 토큰 만료 시간 설정
  • 토큰은 클라이언트가 가지고 있고, 만료 시간이 지나면 더 이상 유효하지 않음
  • 만료된 토큰을 가지고 API 요청 시, 서버에서 이를 검증하고 거부
  • 토큰을 갱신하려면 새로 로그인 or 리프레시 토큰을 이용해 새로운 액세스 토큰을 받아야 함

세션 갱신

  • 서버나 클라이언트에서 관리되며, 세션이 유지되는 동안 인증된 상태로 취급
  • 세션 갱신은 사용자가 활동할 때마다 세션 만료 시간을 연장시키는 과정
  • 30분동안 아무작업없으면 세션 만료, 20분째 새로운 요청 보내면 세션 만료 시간 다시 연장되어 또 다시 30분 동안 유효되는 시스템

토큰은 특정 시간 지나면 더 이상 사용할 수 없도록 제한, 한번 발급된 토큰은 만료 시 무조건 재발급

세션 갱신은 활동할 때마다 세션을 연장해서, 일정 시간 동안 사용자 인증 상태가 유지.
서버에서 관리되는 세션이기 때문에 세션 갱신은 주로 서버 또는 클라이언트와 서버 간의 통신에서 이뤄짐

세션 갱신쪽에서 nextAuth에서 사용할 수 있는 옵션은 maxAge, updateAge가있는데

maxAge - 설정한 시간동안 아무 활동 없으면 세션 만료
updateAge - 사용자가 요청 보낼 때마다 세션의 남은 시간이 30분으로 연장

근데 사실상 세션 인증방식이라 생각하고 적용했지만 session인증 방식 자체를 jwt 즉 토큰 방식을 택했기 때문에 maxAge나 updateAge는 세션이 아닌 jwt 즉 토큰에 적용되는 설정임

토큰이 탈취될 가능성이 있으므로 보안을 추가해주고 싶은데,
이 부분은 추후 상의를 통해 결정할 예정

nextauth session에 닉네임, 프로필이미지 사용자 정보 추가하여 로그인 사용자 데이터 api 호출 제거 + useEffect 의존성 배열 요소 최소한으로 설정 - 성능 향상

기존에는 로그인한 사용자 데이터를 api 호출을 통하여 가져왔었는데, 이번 nextauth쪽 리펙토링을 하다보니, jwt나 session쪽에 사용자 정보를 담을 수 있는데 그걸 따로 추가적으로 사용하진 않고 있었다. 단지 session에 데이터가 담겨있으면 로그인상태구나 인지하는정도?만 사용했었는데, 이제 로그인한 유저 데이터까지 여기서 관리해서 사용하면, 기존에
여기 상단 혹은 팔로우를 진행했을때 목록에 뜨는 프로필 이미지 정보를 호출해야하기 때문에, 서버 과부하가 일어날 수 있었다. 그래서

jwt와 session에 닉네임과 프로필이미지까지 추가해주고,

기존에 api호출을 통해 불러오던 로그인 유저 정보를 session에서 불러와서 호출 자체를 줄일 수 있었다. 매우 매우 긍정적인 리펙토링!!
그리고 의존성 배열에 들어가는 요소는 기존 5개에서 status, session 등 최소한의 요소만 넣어서 리렌더링 최소한으로 구성

158회 호출 -> 113회 호출 -> 0회 호출

CRUD갑자기 안되던 원인 알아냄 => 수파베이스 서비스 롤 키 활성화 + RLS 정책 강화 + 업데이트 함수 api호출방식으로 변경

전에 기능 구현만 초점을 맞추다보니, CRUD가 잘되었었다가 route.ts에서 API 호출하는 방식으로 변경하니 잘되던 CRUD가 막혔던 일이 생각났다. rls정책을 정말 최소한으로 막아놔서 겨우 겨우 crud를 해결했던 기억이 있는데, 오늘 env.local쪽을 보다보니, SUPABASE_SERVICE_ROLE_KEY를 비활성화 한걸 발견하였다. ANON_KEY만 활성화해서 잘 구현되고 있긴해서 그냥 지울까 싶었지만, 과거의 내가 저걸 건든 이유가 있을거라 생각해서 좀 알아봤는데,
최초 클라이언트 측에서 직접 데이터를 호출하던 방식에서 route.ts 즉, 서버에서 실행되는 코드로 바꾸면서 정확하게 에러가 발생을 했었는데, 알고보니, 서버에서 비공개 키를 사용하면 높은 권한을 가진 키로 보안을 강화하여 데이터를 가지고 온다는 사실을 이번에 알게되었다.

import { createClient } from "@supabase/supabase-js"
import { Database } from "@/types/supabase"

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY as string

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)

기존에는 이렇게 AnonKey만 사용해서 잘만 CRUD를 구현했는데, API 호출하는 방식으로 바뀌게 되면서 CRUD를 불러오는 키에 권한이 부족해서 원인이 발생했던 것이였고,

익명 키 (NEXT_PUBLIC_SUPABASE_ANON_KEY)

  • 용도: 주로 클라이언트 애플리케이션에서 사용, 사용자가 애플리케이션에 접근할 때 클라이언트가 이 키를 사용하여 Supabase와 통신
  • 권한: 인증된 사용자만 액세스할 수 있는 테이블에 대한 읽기 및 쓰기 권한이 제한 즉, 특정한 정책(Row Level Security, RLS)에 따라 액세스가 제한

즉 이전엔 이 키만 사용하다보니 서버에서 코드가 실행되는 환경인 route.ts에선 당연히 rls정책에 제한되는 상황이 많았던 것이다.

서비스 역할 키 (SUPABASE_SERVICE_ROLE_KEY)

  • 용도: 주로 서버에서 사용, 서버 측 코드에서 데이터베이스에 대한 높은 권한의 작업을 수행할 때 사용
  • 권한: 모든 데이터베이스 작업을 수행할 수 있는 권한, RLS 정책이 적용되지 않으며, 모든 데이터에 접근할 수 있음.

그래서 rls정책을 대폭 완화시켜서 CRUD를 해결했었었는데,

한번 이전 처럼 rls 정책을 강화하고 CRUD를 진행해보겠다.

좀 더 강화된 rls정책을 세팅해줘도 다행히 잘 진행이 되었는데, 저 중에 게시글 수정만 안되고 있었다.

확인해보니,
기존 직접 클라이언트측에서 데이터요청하는 로직을 그대로 사용하고 있었고,

다행히 발견해서 api 호출하는 방식으로 변경

그래서 업데이트부터 전체 CRUD 잘 해결

  • vercel에 supabase_service_role_key 추가

리엑트쿼리 추가 적용 - devtools 설치 + lazy 적용(성능 최적화)

react query를 이전에는 useInfinitequery 즉 무한스크롤만 적용했지만 이제 비동기 즉, 서버 데이터 처리하는 과정을 다 리엑트 쿼리로 디벨롭해서 성능 향상을 진행할 것이기 때문에,
devtools도 처음 설치해보았는데,
모든 쿼리 결과, 로딩 상태, 에러 상태, 캐시 데이터를 개발자 도구에서 시각적으로 확인
특정 쿼리 상태 쉽게 확인하고, 수동 재실행 및 초기화
디버깅,
쿼리 호출빈도수, 비효율적 동작하는게 있는지 파악해서 성능개선 용도가 있어서 설치!!

여기서 쿼리란
서버로부터 데이터 가져오는 요청

그냥 설치만 했다가 docs에 보니 react.lazy를 적용하면 번들에 포함되서 항상 실행되지 않고, reactquerydevtools가 필요할 때만 로드되게 함. 즉, 페이지가 로드될 때 devtools의 코드는 로드되지 않고, 조건이 충족되었을 때 (개발모드에서)로드됨
그래서 초기 번들 크기가 줄어들고, 로딩속도가 개선됨. 실제 특정 컴포넌트나 기능이 필요할때만 로드가 됨.
그 과정에서 Suspence도 사용하였는데, 비동기 로딩을 관리할 때 사용했음.
그래서 React.lazy로 로드한 컴포넌트는 처음 로드될 때 코드가 준비될 때 까지 잠시 대기하게 되는데, 그 과정에서 Suspence가 없으면, React는 로딩 상태를 관리할 수 없어서 오류가 발생할 수 있음.

"use client"

import React, { Suspense } from "react"

// React.lazy를 사용해 ReactQueryDevtools를 지연 로딩
const ReactQueryDevtoolsProduction = React.lazy(() =>
  import("@tanstack/react-query-devtools/production").then((d) => ({
    default: d.ReactQueryDevtools,
  })),
)

const ReactQueryDevtoolsWrapper = () => {
  return (
    <Suspense fallback={null}>
      {process.env.NODE_ENV === "development" && (
        <ReactQueryDevtoolsProduction initialIsOpen={false} />
      )}
    </Suspense>
  )
}

export default ReactQueryDevtoolsWrapper

사실 구현하다보니 알게된 사실인데 과거의 내가 이미 devtools는 세팅을 하긴 했었다.ㅋㅋ;; 기억을 못함.
어쨋든 중요한 사실은, lazy로 초기 번들러에 코드가 포함되지 않게 구현해서 초기 로딩 속도 개선을 조금이라도 할 수 있지 않았을까 생각해본다.

zustand 회원가입 정보 전역상태관리(회원가입 후 바로 로그인페이지에 작성한 이메일 뜨게)

zustand를 구현을 전에 시도했다가 굳이 사용할 곳을 못찾아서 적용을 하지 않았었는데, 회원가입성공하고 로그인 페이지로 넘어가면 그 정보를 그대로 넘겨줘서 로그인 버튼만 눌러서 바로 넘어가도록 조치를 취하기로 정하였다.

Zustand vs Redux

상태 관리 모델

  • Redux
    • 불변 상태 모델 기반 동작
    • 앱 전역에서 사용할 상태는 reducer함수로 정의, 액션 타입에 따라 상태 업데이트
    • 전역 상태를 사용하려면 Provider로 앱을 감싸야하고, 상태는 useSelector와 useDispatch로 사용
  • Zustand
    • 불변 상태 모델 따르지만 Provider 필요가 없음
    • 상태 관리 훅을 만들고, 훅으로 상태를 공유하고 변경 가능(store / 훅컴포넌트 / ui 컴포넌트)
    • 상태는 useStore라는 훅을 사용하여 조회하고 업데이트

코드 복잡도

  • Redux
    • 상대적으로 복잡, 초기 설정 시 reducer, action, store를 정의해야함
    • 상태 증가 -> 여러 action reducer 정의해야해서 코드 장황해짐
    • redux toolkit 사용 시 복잡도는 완화되도 상태 관리 구조 자체가 복잡한 default값
  • Zustand
    • 상태 관리 매우 간단하고 직관적, 상태, 상태 변경 함수 정의하는 방식 함수형 프로그래밍
    • 상태 변경 로직을 한 곳에 모아두지 않아도 되며, 더 간단한 인터페이스로 상태 관리 가능

Provider 사용 여부

  • Redux : 반드시 애플리케이션을 Provider로 감싸야만 전역 상태 사용 가능
  • Zustand : Provider 없이도 전역 상태 쉽게 사용 가능. 컴포넌트에서 useStore 훅을 호출하면 됨

상태 업데이트 방식

  • Redux
    • 상태 업데이트하려면 dispatch로 액션 보냄. reducer는 해당 액션을 받아 상태 업데이트
    • useDispatch로 action 디스패치하고, 상태 useSelector로 조회
    • redux Toolkit은 상태 변경이 좀 더 쉽지만 여전히 action을 dispatch하는 구조임
  • Zustand
    • 직접적으로 상태 업데이트 가능. 상태와 함께 상태 변경 함수 (set)을 정의하여 필요할때 바로 업데이트
    • 액션 디스패치 없이 상태 변경 함수 호출

성능 최적화

  • Redux
    • useSelector 사용 시 리렌더링 최적화 위해 Memoization같은 기법 사용해야함
    • Redux의 상태 변화는 컴포넌트가 항상 리렌더링 되는 방식, 불필요한 리렌더링 막기 위해 selectors, reselect 사용해함
  • Zustand
    • useStore에서 상태의 특정 부분만을 선택하여 사용하면 자동 리렌더링 최적화
    • 리렌더링 최적화 기본적으로 내장, 상태 변경이 발생한 부분만 리렌더링해서 성능 관리 더 수월

결론

Redux는 보다 전통적인 Flux패턴(단방향 데이터 흐름), 복잡한 상태 관리와 많은 상태에 적합, 구조 복잡하고 boilerplate가 많아도 대규모 애플리케이션에선 강력한 상태 관리 도구
Zustand는 간결하고 직관적, 설정 없이도 빠르게 사용할 수 있는 상태 관리 라이브러리.
Redux와 비교해 코드량 적고 상태 관리가 단순해 더 빠르게 구축

처음엔 zustand를 사용하고 싶은 마음에 어디다 적용할까 고민하다가, 회원가입 후 바로 상태가 다 적혀있어서 로그인 버튼만 누르면 바로 접속 되도록 조치를 취해주고 싶었는데(회원가입했다고 바로 로그인상태로 만드는거까진 별로라 생각)

그래서 이메일과 비밀번호를 처음엔 다 회원가입 후 로그인 페이지로 넘겨주자 라고 생각했었다가, 지금 로그인 유저 데이터는 nextauth의 jwt로 관리하여 session에 보내줄때 그 값을 가져와서 사용하고 있었는데, 비밀번호를 토큰에 저장하면 탈취의 위험이 매우 있다 생각하여 이메일만 보내주기로 하였다.

일단

import { create } from "zustand"

type UserStore = {
  email: string
  setEmail: (email: string) => void
}

const useUserStore = create<UserStore>()((set) => ({
  email: "",
  setEmail: (email) => set(() => ({ email })),
}))

export default useUserStore

Store > useUserStore.ts 파일을 생성해서
typescript를 적용한 타입을 create Store에 넣어주고, 기본 email값, 상태 감지하여 변화될 setEmail값을 세팅해줬다.

그 후 나는 useState로 이메일 비밀번호 상태 관리를 한것이 아닌, useState에서 react-hook-form의 form 객체에 데이터를 저장하는 방식 그 후에 로그인하고 나서는 인증을 위해 nextAuth의 jwt방식을 이용해 session에 유저 데이터를 저장시켰으므로, session에 있는 데이터 값을 불러와서 메인페이지에 사용했다면, 회원가입페이지-> 로그인페이지로 데이터 상태를 전달하려면 아직 session에 값이 담아져있지 않기 때문에 zustand가 필요했다.

그래서 회원가입 처리 함수에서 setEmail에 email정보를 담아줬고,

  const handleSignUp = async (data: userType) => {
    const { email, password, name, nickname } = data
    setLoading(true)

    try {
      const response = await fetch("/api/auth/sign-up", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ email, password, name, nickname }),
      })

      const responseData = await response.json()
      if (!response.ok) {
        throw new Error(responseData.error || "회원가입에 실패하였습니다.")
      }
      // 이메일 Zustand에 저장
      setEmail(email)

      toast({
        title: "회원가입 성공",
        description: "회원가입이 성공적으로 완료되었습니다.",
      })

      route.push("/login")
    } catch (error: unknown) {
      handleError(error)
      toast({
        title: "회원가입 실패",
      })
    } finally {
      setLoading(false)
    }
  }

실제 ui컴포넌트인 registerPage에선 Form 옵션 중 defaultValues에 저장할 email을 넣어줬다.

그 후 로그인 처리 함수에선 setEmail이 아직은 필요 없고, 로그인페이지 ui컴포넌트에만 최초 defaultValue값에 email을 담아줘서, 이미 회원인 유저는 공백의 email input란을 갓 회원가입을 진행한 유저는 email값이 담기게 세팅을 하였다

바로 잘 담기는 모습

form 코드 리펙토링(중복 코드 多)[공용컴포넌트(props) => children 적용한 공용 컴포넌트]

로그인 / 회원가입쪽을 useState의 상태에서 react-hook-form으로 폼을 구현하였는데 중복되는 코드가 많아서 components>common>CommonInputField.tsx에서 중복 코드를 처리하기로 하였다.

기존 코드를 보면

        <Form {...form}>
          <form onSubmit={form.handleSubmit(handleLogin)} className="space-y-8">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormControl>
                    <Input
                      type="email"
                      placeholder="이메일"
                      {...field}
                      className="p-4 text-lg rounded-lg"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormControl>
                    <Input
                      type="password"
                      placeholder="비밀번호"
                      {...field}
                      className="p-4 text-lg rounded-lg"
                      autoComplete="new-password" // 비밀번호 자동완성방지
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />        <Form {...form}>
          <form onSubmit={form.handleSubmit(handleLogin)} className="space-y-8">
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormControl>
                    <Input
                      type="email"
                      placeholder="이메일"
                      {...field}
                      className="p-4 text-lg rounded-lg"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormControl>
                    <Input
                      type="password"
                      placeholder="비밀번호"
                      {...field}
                      className="p-4 text-lg rounded-lg"
                      autoComplete="new-password" // 비밀번호 자동완성방지
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

이런식으로 겹치는 부분이 많은데,
FormControl, FormField, FormItem, FormMessage를 빼기로 하였다.

type CommonInputFieldProps<TFormData extends FieldValues> = {
  form: UseFormReturn<TFormData>
  name: Path<TFormData>
  placeholder: string
  type: string
  className: string
  autoComplete?: string
}

props의 타입을 정의한 부분. 제네릭 TFormData를 사용하여 다양한 폼 데이터 처리할 수 있게 도와줌

제네릭이란
함수, 클래스, 컴포넌트 타입을 만들 때 타입을 미리 지정하지 않고 사용하는 시점에서 타입 지정하는 기능.
여러 타입을 지원하는 재사용 가능한 코드를 작성할 수 있게 해줌
any랑 헤깔릴 수 있는데, any로 타입을 선언하면 어떤 타입이든 받을 수 있지만, 반환 값의 타입은 미리 알 수 없음

ex - function a(b:any):any {
	return b;
}

이러면 b는 어떤 타입도 받을 순 있어도 반환 값의 타입을 미리 알수는 없음 -> 타입 안전성 떨어짐

ex - function a<T>(b:T): T{
	return b;
}

<T>:여기서 T는 제네릭 타입 매개변수로 함수가 호출될 때 T의 실제 타입이 정해짐.
실제 전달된 값의 타입을 그대로 반환해서, 그 타입이 자동으로 결정 됨
즉 props로 어떻게 타입을 받아오냐에 따라 return 값도 그 타입을 자동으로 따라가게 조치함

<TFormData extends FieldValues>
TFormData는 FieldValues를 확장한 타입으로, 어떠한 데이터 구조여도 허용하지만?? 기본적으로 폼 데이타입은 보장해줌
form:UseFormReturn<TFormData>
useForm에서 반환된 form 객체를 받아오는 속성
control, handleSubmit 등 폼의 상태를 관리하게 해줌
name:Path<TFormData>
폼 필드의 이름을 나타내는 속성.

Path<TformData>란?
react-hook-form에서 폼 필드의 경로를 타입으로 안전하게 지정하는 방법
즉, 폼 데이터 객체의 속성들 중 하나를 name으로 사용할 때, 잘못된 이름을 입력하는 실수를 방지
실제 Path<T>타입은 제네릭과 타입스크립트의 keyof 연산자를 결합하여 동작했다고 볼 수 있는데,
Path<TFormData>는 폼 데이터 객체(FormData)에 정의된 속성들만 허용하게 됨.

  • keyof 연산자
    특정 타입에서 모든 속성 이름의 타입을 가져오는 연산자
type FormData = {
	username:string;
    email:string;
};
type FormDataKeys = keyof FormData; // 'username' | 'email'

즉 keyof 연산자는 FormData 타입의 모든 속성 이름을 추출하는 역할
그렇다면 Path<T>는 react-hook-form에서 제공하는 타입으로, 폼 데이터 구조에서의 경로를 나타내는 타입
이것은 T 타입의 모든 속성 경로를 표현할 수 있음(FormData 타입)
FormData 타입 안에 email 등등 useForm 안에 있는 속성 이름이 있다면 그걸 name으로 사용할 수 있음

  const form = useForm<userType>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      email: email,
    },
  })

여길 보면 useForm은 userType을 타입으로 정의하여 사용하고 있는데, 이 userType은 zod 스키마 LoginSchema로부터 정의된 타입임을 뜻함

실제로 LoginSchema지만 더 많은 타입을 포함하고 있는 RegisterSchema의 타입들을 login쪽에서도 넣어줬는데, 속성명은 email,password,name,nickname이니까 Path<T>는 email,password,name,nickname을 name으로 가져다 쓸 수 있는 것임.
이런식으로 다양한 어떤 타입과 어떤 명칭이 들어갈지 모르는 name에 대해 타입정의를 내릴땐 Path를 통해서도 가능하다는 사실을 이번에 알게 되어서 기쁘다 ㅎㅎ

placeholder / type / className / autoComplete 다 string type만 받아오게 처리했고, optional한 autoComplete는 ?를 넣어서 선택적 옵션임을 나타냄

autoComplete?: string에서 ?란?
옵셔널 속성. TypeScript에서 속성 이름 뒤에 ?를 붙이면 필수가 아닌 선택사항이 됨.
즉 이 속성이 제공되지 않아도 에러가 발생되지 않음

const CommonInputField = <TFormData extends FieldValues>({
  form,
  name,
  placeholder,
  type = "text",
  className = "p-4 text-lg rounded-lg",
  autoComplete,
}: CommonInputFieldProps<TFormData>) => {
  return (
    <FormField
      control={form.control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <FormControl>
            <Input
              {...field}
              type={type}
              placeholder={placeholder}
              className={className}
              autoComplete={autoComplete}
            />
          </FormControl>
          <FormMessage />
        </FormItem>
      )}
    />
  )
}

제네릭 <TFormData extends FieldValues> 사용하여 다양한 폼 데이터를 유연하게 처리 가능.
즉 TFormData는 각기 다른 폼 데이터를 받아올 수 있음
form : useForm에서 반환된 객체, form.control을 사용하여 폼 필드를 제어
name : 폼 필드의 이름, 경로(Path<TFormData>)로 전달 받음
placeholder: 인풋 필드의 플레이스 홀데 텍스트
type:기본 값 text(string)인데 필요에 따라 다른 타입 전달할 수 있음
className:인풋 필드 스타일
autoComplete:자동완성 기능

FormField:react-hook-form과 UI 라이브러리의 form 시스템 결합하는 컴포넌트

  • control={form.control}:폼 상태 관리 위해 react-hook-form의 control 객체를 넘김
  • name={name}: 필드 이름 설정
  • render={({ field})} => ...} : render속성을 통해 field 객체를 전달받습니다. field에는 해당 인풋 필드를 제어하기 위한 여러 속성들과 함수 포함

FormItem,FormControl,Input,FormMessage

  • FormItem : 하나의 폼 필드 항목 나타내는 UI컴포넌트
  • FormControl : 폼 필드 감싸는 컨테이너 역할
  • Input : 실제 인풋 필드. field 객체를 스프레드 연산자로 전달하여 제어
  • FormMessage : 해당 필드에 오류가 발생했을 때 에러 메세지 표시

    스프레드 연산자란?
    ... 세 개의 점으로 표현
    객체나 배열의 내용을 펼쳐서 다른 객체나 배열에 복사하거나 확장
    const arr1 =[1]; const arr2 = [...arr1,2] //[1,2]
    그러면 실제로 <Input {...field} /> 이렇게 쓰였는데
    여기서 ...field는 field 객체 안의 모든 속성 (ex- name,value, onChange 등등)을
    Input 컴포넌트에 전달하여 react-hook-form에서 제공하는 필드의 제어 관련 속성들을 Input에 그대로 넘겨서, 해당 폼 필드를 자동으로 제어할 수 있게 됨

그래서 실제 맨 처음 캡쳐한 화면처럼 되어있는 코드를 CommonInputField 컴포넌트를 넣어줘서 가독성 있고 코드량을 줄여주면 이렇게 된다

실제 코드에 비교하면 엄청나게 줄어든 모습!! 바로 이거지~!

근데 막상 다 작성하고 보니 type에도 placeholder, type, name 등등 온갖 속성에 타입을 선언해줘야하고, props로 받아오는 코드에도 저 속성을 넣어주고, Input안에도 다 넣어줬는데? 결국은 실제 ui컴포넌트에서도 그걸 그대로 사용하고 있으니, 중복코드를 좀 줄이긴 했지만 오히려 중복코드가 늘어난쪽도 있다고 판단이 되었다.

그래서 확인해보니 children으로 자식 요소인 Input은 ui컴포넌트측에서 알아서 세팅을 하도록 조치를 취하는게 중복 코드를 줄일 수 있겠다는 생각이 들었다.

<children 사용 기본 개념>
컴포넌트가 호출될 때 태그 내부에 포함된 내용이 그 컴포넌트의 자식으로 전달

const ParentComponent = ({ children }: { children: React.ReactNode }) => {
  return <div>{children}</div>
}

const App = () => {
  return (
    <ParentComponent>
      <h1>Hello, World!</h1>
      <p>This is a paragraph inside ParentComponent.</p>
    </ParentComponent>
  )
}

ParentComponent는 children을 인자로 받아서 div 내부에 렌더링함
그러면 h1과 p가 children으로 전달되어 해당 위치에 렌더링 됨
<children 상세한 동작 설명>

  • children 타입
    • react에서 React.ReactNode 타입으로 정의. 문자열 숫자, JSX엘리먼트 배열, 또는 null undefined 포함할 수 있는 타입
    • React.ReactNode는 여러 타입을 지원해서, 컴포넌트가 받는 자식들이 꼭 JSX 엘리먼트 아니여도 동작
  • 유연성
    • children 사용하면 컴포넌트 외부에서 그 컴포넌트의 자식 구조를 자유롭게 정의할 수 있음
    • 고정된 UI가 아닌 사용자가 원하는 구조를 담아 재사용성 높임
    • props랑 같이 병행해서 사용도 가능

총정리
컴포넌트 재사용성
UI 구조 유연성
복잡한 UI구조 쉽게 관리

결국 이전 코드에 비해서 훨씬 간결하고 재사용성도 늘린 공용 컴포넌트+실제 ui컴포넌트(로그인/회원가입)를 완성하였는데

//공용 컴포넌트
import {
  ControllerRenderProps,
  FieldValues,
  Path,
  UseFormReturn,
} from "react-hook-form"
import { FormControl, FormField, FormItem, FormMessage } from "../ui/form"
import { ReactElement, ReactNode } from "react"

type CommonInputFieldProps<TFormData extends FieldValues> = {
  form: UseFormReturn<TFormData>
  name: Path<TFormData>
  children: (
    field: ControllerRenderProps<TFormData, Path<TFormData>>,
  ) => ReactElement
}

const CommonInputField = <TFormData extends FieldValues>({
  form,
  name,
  children,
}: CommonInputFieldProps<TFormData>) => {
  return (
    <FormField
      control={form.control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <FormControl>{children(field)}</FormControl>
          <FormMessage />
        </FormItem>
      )}
    />
  )
}

export default CommonInputField
//실제 ui컴포넌트 쪽
        <Form {...form}>
          <form
            onSubmit={form.handleSubmit(handleSignUp)}
            className="space-y-4"
          >
            <CommonInputField form={form} name="email">
              {({ ...field }) => (
                <Input
                  {...field} // field 객체의 속성을 Input에 전달해야 react-hook-form에서 제어가 가능
                  type="email"
                  placeholder="이메일"
                  defaultValue={email}
                />
              )}
            </CommonInputField>

결론적으로는 훨씬 중복코드도 줄이고 사용하기도 좋아진 코드로 완성된 것 같은데,
일단 여기서도 처음 사용하는 타입들이 있어서 확인해보자면
공용 컴포넌트쪽
FieldValues
이전에도 정리하긴했지만, 제네릭 TFormData를 확장하는 형태 -> react-hook-form에서 제공하는 기본 타입
폼 필드 값들이 어떤 객체 형태로 관리될지 정의해줌
Path<TFormData>
제네릭으로 받은 폼 데이터 내에서 유효한 키(name 속성에 사용될 필드이름)를 타입으로 지정
UseFormReturn<TFormData>
useForm 훅에서 반환되는 객체의 타입. 이 객체는 폼을 제어하는데 필요한 메서드와 값을 포함함
control/handlesubmit/reset 등 메서드 포함되어있고, 폼의 제어방법을 제공해줌
ControllerRenderProps<TFormData, Path<TFormData>
ControllerRenderProps는 Controller나 FormField에서 사용될 때, react-hook-form이 자동으로 전달하는 field 객체의 타입
field객체에 포함된 프로퍼티들을 정의하고 있고, 이 겍체엔 폼 필드가 실제로 제어되는 값과 이벤트 핸들러들이 포함됨(field.onChange, field.onBlur, field.value 등)
제네릭으로 TFormDataPath<TFormData>를 받아 폼 필드의 경로에 맞는 필드의 타입을 결정
즉, name과 field가 정확하게 타입 추런할 수 있게 됨

그리고 지금은 ReactElement로 처리해서 해결되었지만 이젠에는 ReactNode로 사용하여 input이 화면에 뜨지 않는 문제가 있었는데

  1. ReactNode
    JSX에서 렌더링 가능한 모든 요소들을 포함하는 매우 넓은 타입
    그래서 타입이 너무 넓어서 field를 인식 못하거나 타입 추론이 깨지게 됨

  2. field 객체 전달 문제
    이전에는 render 함수 내부에서 field 객체를 전달하지 않거나, ReactNode로 인해 제대로 전달되지 않아, children 내부에서 field 객체가 제대로 활용되지 않았음.
    render함수는 field를 통해 Input과 같은 JSX요소에 react-hook-form의 상태를 연결하는 중요한 역할

결론적으로는 이게 좀 더 중복코드는 중복코드대로 낱낱이 구현하는 코드는 개개인별로 적용하는 최적화된 컴포넌트 리펙토링이 아닐까 싶다.

비밀번호 자동 완성 방지

Input 옵션의 autoComplete 기능을 활성화하고 'new-password' 속성 추가해서 브라우저 자동 완성 기능 방지하도록 기능 추가

<Input
type="password"
placeholder="비밀번호"
{...field}
className="p-4 text-lg rounded-lg"
autoComplete="new-password" // 비밀번호 자동완성방지
/>
profile
웹 개발자 되고 시포용

0개의 댓글