-
[D'art] 로그인 / 회원가입 기능 구현하기개발일기장/Next.js로 리팩토링하기 2025. 2. 21. 08:45
정보처리기사 실기를 준비하느라 프로젝트에 대해서 좀 소홀했던 것 같다. 다시 시작한다는 마음으로 오늘은 꼭 회원가입 / 로그인을 구현하고 말겠어...
📋 목표
- 로그인 UI 완성하기.
- 로그인 기능 구현하기.
- 회원가입 UI 완성하기.
- 회원가입 기능 구현하기.
- Supabase와 연동하기.
- 코드 리뷰 및 개선 점 찾기.
🖥️ 진행 사항
1️⃣ 회원가입 UI 완성하기
1-1. 약관동의 상태관리 어떻게 할까?
일단 상위 노드에서 하위 노드의 값을 조절하는 방식으로 하는 것이 좋다고 생각이 들었으며 check 여부를 최대한 간단히 나타내고자 하였다.
"use client"; ... const SignupAgreePage = () => { const [agreeState, setAgreeState] = useState<boolean[]>([ false, false, false, ]); return ( <section className="h-fit w-full"> <p className="text-4xl">약관동의</p> <Line /> <div className="flex flex-col gap-6 px-4"> {agreeInfo.map(({ desc, title }) => ( <SignupCheckBox key={title} title={title} desc={desc} /> ))} </div> ... </section> ); }; export default SignupAgreePage;
- 일단 Page 컴포넌트에서 client component로 구성되어있는 부분을 수정할 수 있지 않을까? 생각이 들었다. 근데 이렇게 되면 boolean값의 여부에 따라서 button을 활성화할지말지에 대해서 구성하기가 귀찮아지는 부분이 있다. 재활용성도 떨어지게 될 거 같은 부분이 있어서 일단은 그대로 가자.
1-2. "use client"를 비활성화시키려는 노력은 도움이 될까?
next.js를 사용하는 이유 중 하나가 server component의 활용에 있다고 생각한다. 그렇기에 일반적으로 client component의 수를 줄이는 것이 도움이 될 것이라고 생각했는데 성능으로 내가 고치려고 맘 먹은 부분을 분석해보았다.
회원가입 시 약관동의페이지 해당 페이지의 정보를 보면 서버와의 송수신이 없는 단순히 hook과 state를 활용한 페이지이기에 'use client'를 자연스럽게 붙일 수 밖에 없던 페이지인데 성능면으로는 전혀 문제가 없음을 보인다. 이에 따라 송•수신까지 하는 정도 아니라면 굳이 server component에 집착할 필요 없을 것 같다. 접근성 깎아먹은 5점이나 올리는 노려을 담에 해보자(대충 봤는데 url이나 기타 등등의 요소가 들어가는 것 같더라)
1-3. middleware를 사용해서 2 step의 signup단계에서 이전 단계를 필수로 거치게 하는 방법은?
처음에는 localstorage에 값을 저장하고 middleware에서 이를 불러오면되겠다고 생각을했지만 실제로는 middleware는 서버에서 실행되기에 localstorage를 불러올 수가 없는 상황이다.
오류 구문 - 해당 방법을 cookie를 통해서 해결을 했다. localstorage는 클라이언트에서 작동하는 반면에 cookie를 통해서 http 요청에 내가 원하는 값을 middleware에서 참조해서 사용할 수 있었다.
// middleware.ts import { NextResponse } from "next/server"; import { NextRequest } from "next/server"; const allowedPaths = [ ... ]; export function middleware(request: NextRequest) { const url = request.nextUrl.pathname; ... // 회원가입 시 step1을 거치지 않았다면 접근할 수 없음 const agreement = request.cookies.get("agreement")?.value || false; if (url === "/signup/info" && !agreement) { return NextResponse.redirect(new URL("/login", request.url)); } return NextResponse.next(); } ... // set cookies handler const onClickNextButton = () => { if (!disabled) { router.push("/signup/info"); document.cookie = "agreement=true; path=/; max-age=2592000"; // 30일 유지 } };
1-4. email과 nickname에서는 모달을 띄울건데 모달은 어디에 위치하고 어떻게 구현하는 게 좋을까?
일단 portal을 사용해서 모달을 구현해놓은 기존의 상태인데 뭔가 파일구조가 복잡하고 귀찮아보이는 느낌이 있다. 그래서 몇가지 부분을 수정하고자 한다.
- modal Portal을 전역변수로 사용해서 button에 handler와 띄우고 싶은 modal의 정보만 전달하는 방법은 어떨까?
- 이 때 provider에 전역적으로 modal을 설정해 놓는다면 어디서든 사용이 가능하지 않을까?
- zustand를 활용해서 좀 더 쉽고 간단하게 구현해보자!!
기존 코드
더보기// usage "use client"; ... const InputWithModal = (...) => { const { isExpand, onToggle } = useOutsideClick(); return ( <div> <div className="flex flex-row items-end gap-10"> <InputField {...props} disabled /> <button className="h-10 w-20 min-w-24 border text-xs" onClick={onToggle} > {buttonLabel} </button> </div> {isExpand && ( <ModalPortal> ... </ModalPortal> )} </div> ); }; ... export default InputWithModal; /** 기존에 사용하던 코드에서는 portal이 해당 컴포넌트에 의존하게 된다는 단점이 생기는 것 같다. */ // Modal Portal "use client"; import { PropsWithChildren, useEffect, useState } from "react"; import { createPortal } from "react-dom"; const ModalPortal = ({ children }: PropsWithChildren) => { const [modalRoot, setModalRoot] = useState<HTMLElement | null>(null); useEffect(() => { setModalRoot(document.getElementById("modal-portal")); }, []); return ( <> {modalRoot ? createPortal( <div className="modal-positon bg-black bg-opacity-20 backdrop-blur-md"> {children} </div>, modalRoot, ) : null} </> ); }; export default ModalPortal; /** 나중에 알림을 띄울 alert창의 경우에는 모달이 띄어진 상태에서도 사용될 수 있으니깐 분리해서 사용하자 */
개선된 코드
더보기// modalPortal.tsx "use client"; import { useModalStore } from "@/store/modal"; import SignupEmailCheckBox from "@/ui/signup/signupEmailCheckBox"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; const ModalPortal = () => { const [modalRoot, setModalRoot] = useState<HTMLElement | null>(null); const type = useModalStore((state) => state.type); useEffect(() => { setModalRoot(document.getElementById("modal-portal")); }, []); return ( <> {modalRoot ? createPortal( <div className="bg-black bg-opacity-20 backdrop-blur-md modal-positon"> {render(type)} </div>, modalRoot, ) : null} </> ); }; const render = (type: string) => { switch (type) { case "email": return <SignupEmailCheckBox />; } }; export default ModalPortal; // store with zustand import { create } from "zustand"; interface ModalState { isExpended: boolean; type: string; expendModal: (type: string) => void; closeModal: () => void; } export const useModalStore = create<ModalState>((set) => ({ isExpended: false, type: "email", expendModal: (type) => set((state) => ({ ...state, isExpended: true, type: type })), closeModal: () => set((state) => ({ ...state, isExpended: false })), })); // provider "use client"; import ModalPortal from "@/context/modalPortal"; import { useModalStore } from "@/store/modal"; import { PropsWithChildren } from "react"; const ModalProvider = ({ children }: PropsWithChildren) => { const isExpended = useModalStore((state) => state.isExpended); return ( <> {isExpended && <ModalPortal />} {children} </> ); }; export default ModalProvider;
Root Layout에 provider를 위치시키고 usage에서는 zustand의 expendModal을 button의 onClick handler로 사용하는 구조
'개발일기장 > Next.js로 리팩토링하기' 카테고리의 다른 글
[D'art] Next.js로 리팩토링하기 4일차 (0) 2025.02.03 [D'art] Next.js로 리팩토링하기 3일차 (1) 2025.01.28 [D'art] Next.js로 리팩토링하기 2일차 (1) 2025.01.23