ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [D'art] 로그인 / 회원가입 기능 구현하기
    개발일기장/Next.js로 리팩토링하기 2025. 2. 21. 08:45
    정보처리기사 실기를 준비하느라 프로젝트에 대해서 좀 소홀했던 것 같다. 다시 시작한다는 마음으로 오늘은 꼭 회원가입 / 로그인을 구현하고 말겠어...

     

    📋 목표

    1. 로그인 UI 완성하기.
    2. 로그인 기능 구현하기.
    3. 회원가입 UI 완성하기.
    4. 회원가입 기능 구현하기.
    5. Supabase와 연동하기.
    6. 코드 리뷰 및 개선 점 찾기.

    🖥️ 진행 사항

    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을 사용해서 모달을 구현해놓은 기존의 상태인데 뭔가 파일구조가 복잡하고 귀찮아보이는 느낌이 있다. 그래서 몇가지 부분을 수정하고자 한다.
    1. modal Portal을 전역변수로 사용해서 button에 handler와 띄우고 싶은 modal의 정보만 전달하는 방법은 어떨까?
    2. 이 때 provider에 전역적으로 modal을 설정해 놓는다면 어디서든 사용이 가능하지 않을까?
    3. 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로 사용하는 구조

    댓글

Designed by Tistory.