글쓰는쿼카의 PM 여정

[FP] 마이페이지의 닉네임 변경하기 (2024. 7. 29.) 본문

개발/Next.js

[FP] 마이페이지의 닉네임 변경하기 (2024. 7. 29.)

글쓰는쿼카 joymet33 2024. 7. 29. 23:34

#스파르타코딩클럽 #내일배움캠프(프론트엔드_React) 
학습주제 : Next.js - route handler, React useState

학습내용 : 닉네임에 대한 유효성 검사, 닉네임 중복 확인 기능

학습일 : 2024. 7. 29.


 

 

배운 내용 요약

 

기능 기획 단계

1. 무엇을 만들까?

마이페이지에서 닉네임을 변경하는 기능을 만들어보고 싶다.

유저 입장에서 귀찮은 과정은 빠르게 지나가면 좋기 때문에 타자를 치면서 오류 메시지를 바로 볼 수 있게 하고 싶다.

아래 고려해야 할 큰 카데고리 중 2가지만 집중적으로 다루고자 한다.

 

( 👇🏻 고려해야 할 큰 카테고리)

  • UI 뼈대
  • 이벤트 함수
  • 이벤트 함수를 통한 화면 변화
  • 유효성 검사의 기준 (⭐여기!)
  • 데이터베이스 서버 통신이 필요한 부분 (⭐여기!)

 

2. 유효성 검사 기준 정하기

유효한 닉네임인지 아닌지 판가름 하기 위한 기준이 필요하다.

아래 내용은 우리가 흔히 아는 유효성 검사 목록 중 적용해본 것들을 작성해보았다.

4가지 중 하나('중복 확인')는 데이터베이스 서버 통신이 필요하기 때문에 따로 다루도록 하겠다.

 

( 👇🏻 한번쯤 경험해봤을 흔한 유효성 검사 목록)

  • 빈칸 - "빈 곳을 채워주세요."
  • 문자열 길이 - "3자 이상, 12자 이하만 가능합니다."
  • 특수문자 - "특수문자는 사용할 수 없습니다."
  • 중복 확인 - "동일한 닉네임이 있습니다." (⭐DB 서버 통신!)

 

3. 어떻게 만들까?

1) React 활용

타자를 치면서 오류 메시지를 바로 볼 수 있게 하려면 닉네임을 입력하는 인풋 태그의 onClick 콜백함수로 useState 또는 useEffect를 활용하면 좋다. 아래는 코드 작성 플로우를 키워드 위주로 작성한 내용이다.

  • 입력되는 e.target.value를 저장하는 기능
  • 저장된 state를 조건대로 검사하는 함수 실행 --- 다른 페이지(예: 회원가입)에서도 사용할 수 있도록 컴포넌트화!
  • 모르는 건 검색: 이메일 또는 특수문자의 정규식 표현
  • 상태값을 기반으로 삼항연산자를 활용하여 오류 메시지 보여주기

( 👇🏻전체 코드 - 파일 2개)

더보기
// src > app > (main) > mypage > edit > _components > InfoContainer.tsx

export const CheckNicknameValidity = ({
  nickname,
  setNicknameError,
  setIsNicknameValid
}: {
  nickname: string;
  setNicknameError: React.Dispatch<React.SetStateAction<string>>;
  setIsNicknameValid: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
  if (!nickname) {
    setNicknameError("빈칸을 채워주세요.");
    setIsNicknameValid(false);
    return;
  }

  if (nickname.length < 3 || nickname.length > 12) {
    setNicknameError("닉네임은 3자 이상 12자 이하여야 합니다.");
    setIsNicknameValid(false);
    return;
  }
  if (/[^a-zA-Z0-9]/.test(nickname)) {
    setNicknameError("닉네임에는 특수문자를 사용할 수 없습니다.");
    setIsNicknameValid(false);
    return;
  }
  setNicknameError("");
  setIsNicknameValid(true);
};


// src > app > (main) > mypage > edit > _components > checkValidity.tsx

export const CheckNicknameValidity = ({
  nickname,
  setNicknameError,
  setIsNicknameValid
}: {
  nickname: string;
  setNicknameError: React.Dispatch<React.SetStateAction<string>>;
  setIsNicknameValid: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
  if (!nickname) {
    setNicknameError("빈칸을 채워주세요.");
    setIsNicknameValid(false);
    return;
  }

  if (nickname.length < 3 || nickname.length > 12) {
    setNicknameError("닉네임은 3자 이상 12자 이하여야 합니다.");
    setIsNicknameValid(false);
    return;
  }
  if (/[^a-zA-Z0-9]/.test(nickname)) {
    setNicknameError("닉네임에는 특수문자를 사용할 수 없습니다.");
    setIsNicknameValid(false);
    return;
  }
  setNicknameError("");
  setIsNicknameValid(true);
};

2) 라우트 핸들러 활용 - router.ts

닉네임 중복확인을 하기 위해서는 데이터베이스의 유저 정보를 조회해야 한다.

(라우트 핸들러 사용법을 이해하기까지 정말 오래 걸렸는데, 드디어 감🍊을 잡았다)

 

데이터베이스를 활용하려면 서버와 통신하는 방법을 알아야 하는데, 크게 4가지를 알아야 한다.

  • request, response --> 요청과 응답
  • request의 method, headers, body --> 요청의 내용
  • response 상태 200대, 400대, 500대 --> 200대: 요청&응답 정상, 400대: 클라이언트 오류, 500대: 서버 오류
  • response 상태에 따라 직접 error 만들기

 

더보기

( 👇🏻서버 통신 method별 예시)

  • GET : 조회해서 데이터를 가져온다 (특징: request의 body 사용 안 함)
    -> 예시: 게시글 목록 가져오기, userId가 일치하는 유저의 회원 정보 가져오기
  • POST : 최초의 데이터를 만든다 (특징: requestd의 body 사용함)
    -> 예시: 게시글 작성하기, 변경할 닉네임의 중복 확인하기, 로그인⭐⭐⭐
  • PATCH 또는 PUT : 기존에 있는 데이터를 수정한다
    -> 예시: 게시글 수정하기, 닉네임 변경하기
  • DELETE : 데이터를 삭제한다
    -> 예시: 게시글 삭제하기

( 👇🏻전체 코드 - 파일 3개, 마지막 route에 주목!)

더보기
//  src > app > (main) > mypage > edit > _components > UserEditNickname.tsx

"use client";

import { updateNickname } from "@/apis/auth/mypage/mypageInfo";
import { CheckNicknameValidity } from "@/app/(main)/mypage/edit/_components/checkValidity";
import { ChangeEvent, FormEvent, MouseEventHandler, useState } from "react";

function UserEditNickname() {
  const [nickname, setNickname] = useState<string>("");
  const [nicknameError, setNicknameError] = useState<string>("");
  const [isNicknameValid, setIsNicknameValid] = useState<boolean>(false);
  // 유효성 검사 통과할 경우: isNicknameValid === true
  // 유효성 검사 통과 못 할 경우: isNicknameValid === false
  const [nicknameDupError, setNicknameDupError] = useState<string>("");
  const [isNicknameAllPassed, setIsNicknameAllPassed] = useState<boolean>(false);
  // 중복확인 통과할 경우: isNicknameAllPassed === true
  // 중복확인 통과 못 할 경우: isNicknameAllPassed === false

  const handleNicknameChange = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // 유효성 검사
    // CheckNicknameValidity({ nickname, setNicknameError, setIsNicknameValid });

    // 유효성 검사 통화 못 할 경우 중복확인 실시 안 함
    if (!isNicknameValid) {
      setNicknameDupError("");
      setIsNicknameAllPassed(false);
    }

    // 3. 중복확인
    if (isNicknameValid) {
      await updateNickname(nickname, setNicknameDupError, setIsNicknameAllPassed);
    }
  };

  return (
    <div className="AuthInputDiv">
      <label>닉네임 변경</label>
      <form className="AuthInputForm" onSubmit={handleNicknameChange}>
        <input
          type="text"
          value={nickname}
          placeholder="변경할 닉네임을 입력해주세요"
          className="AuthInputShort"
          onChange={(e) => {
            setNickname(e.target.value);
            CheckNicknameValidity({ nickname: e.target.value, setNicknameError, setIsNicknameValid });
            if (isNicknameAllPassed) {
              setNicknameDupError("");
              setIsNicknameAllPassed(false);
            }
          }}
        />
        <button className="AuthDupButton" type="submit">
          중복확인
        </button>
      </form>
      {isNicknameValid ? "" : <p className="AuthStateInfo">{nicknameError}</p>}
      {isNicknameAllPassed ? (
        <p className="AuthStateInfoGreen">{nicknameDupError}</p>
      ) : (
        <p className="AuthStateInfo">{nicknameDupError}</p>
      )}
    </div>
  );
}

export default UserEditNickname;



//  src > apis > auth > mypage > mypageInfo.ts

import { Dispatch, SetStateAction } from "react";

export const updateNickname = async (
  nickname: string,
  setNicknameDupError: Dispatch<SetStateAction<string>>,
  setIsNicknameAllPassed: Dispatch<SetStateAction<boolean>>
) => {
  const res = await fetch("/api/auth/mypage/test", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(nickname)
  });

  if (res.status === 200) {
    setNicknameDupError("사용 가능한 닉네임 입니다");
    setIsNicknameAllPassed(true);
  }

  if (res.status === 409) {
    setNicknameDupError("동일한 닉네임이 있습니다.");
    setIsNicknameAllPassed(false);
  }

  if (res.status === 500) {
    setNicknameDupError("알 수 없는 에러가 발생했습니다.");
    setIsNicknameAllPassed(false);
  }
};



//  src > app > api > auth > mypage > test > route.ts
import { createClient } from "@/utils/supabase/server";
import { NextRequest, NextResponse } from "next/server";

export const POST = async (req: NextRequest) => {
  const supabase = createClient();
  const nickname = await req.json();
  const { data } = await supabase.from("users").select("*").eq("nickname", nickname).single();

  if (!data) {
    return NextResponse.json("사용 가능한 닉네임입니다."); (⭐ 여기!)
  }

  if (data) {
    return NextResponse.json({ error: "동일한 닉네임이 있습니다." }, { status: 409 });
  } (⭐ 여기!)

  return NextResponse.json({ error: "알 수 없는 에러가 발생했습니다." }, { status: 500 });
}; (⭐ 여기!)