티스토리 뷰

1. MSW 세팅과 버전 업그레이드

msw

1. MSW로 미리 API 만들기 - 서버 쪽 세팅

유의. 넥스트에서 매끄럽게 되지 않는 부분이 있다.

1-1) 설치

npx msw init public/ --save // 넥스트 프로젝트 폴더 내에 public 폴더
// y 클릭
npm install msw --save-dev 

Pasted image 20231217211115.png
(1번을 실행하면 public에 mockServiceWorder.js가 실행됨)

🧐 어떤 때에 MSW를 쓰나요?

개발 환경에서 억지로 에러를 내거나 매번 로그인하는 것을 skip할 때 msw를 사용하면 좋다.

1-2) src/mocks 폴더구조

Pasted image 20240114202216.png

Next Server에서는 아직… node 로 대신해보자.

서버, 클라이언트 두 환경 모두 msw가 돌아야 한다.

그런데 아직 Next.js 서버와 자연스럽게 호환되지 않아서 임시로 node 서버를 활용한다.

src/mock/http.ts 확인

npm i -D @mswjs/http-middleware express cors 
npm i --save-dev @types/express @types/cors // ts

실제 코딩은

src/mock/handlers.ts 확인

1-3) src/mock/http.ts, 즉 서버를 띄워놓자 npm run mock

코드가 바뀌면 node를 껐다가 재실행 해줘야한다. 이 과정은 번거롭다.
scripts에 mock 명령어 설정해두고, 서버 띄워서 실행시켜두면 편하다.
Pasted image 20231217213520.png|500

🤝 HTTP header 알고갑시다.

Set-Cookies

Set-Cookies는 웹 서버가 클라이언트의 웹 브라우저에게 쿠키를 저장하도록 지시하는 HTTP 응답 헤더이다.
쿠키는 웹 브라우저에 저장되어 서버와 클라이언트 간의 상태 정보를 유지할 수 있게 해준다.
예를 들면, 로그인 상태, 세션 정보, 사용자 환경 설정 등을 저장할 수 있다.

쿠키의 내용은 아래와 같다.

connect.sid // 쿠키 이름
msw-cookie // 쿠키의 값
HttpOnly (opts) // 이 옵션이 설정되면 클라이언트 측 스크립트에서 쿠키에 직접 접근할 수 없다. (보안 강화 설정)
Path (opts) // 쿠키가 전송되는 경로 지정
Max-Age (opts) // 쿠키의 최대 수명을 초 단위로 설정, 0인 경우는 쿠키 바로 삭제
// [Set-Cookie 헤더를 통해 클라이언트의 웹 브라우저에 쿠키를 저장하도록 지시]
// 
// HTTP 응답 객체를 생성하고, JSON 형식의 사용자 정보 User[1]를 반환하면서 동시에
// Set-Cookie라는 HTTP 응답 헤더를 설정하는 예시.
http.post("/api/login", () => {
	console.log("로그인");
		return HttpResponse.json(User[1], {
			headers: {
				"Set-Cookie": "connect.sid=msw-cookie;HttpOnly;Path=/",
			},
	});
}),

// [웹 브라우저에 저장된 쿠키를 삭제하도록 지시]
http.post("/api/logout", () => {
	console.log("로그아웃");
	return new HttpResponse(null, {
		headers: {
			"Set-Cookie": "connect.sid=;HttpOnly;Path=/;Max-Age=0",
		},
	});
}),

2. next용 msw 컴포넌트와 .env

2-1) 세팅하기

클라이언트 컴포넌트
src/app/_component/MSWComponent.tsx 를 하나 만들고, 상위 layout.tsx에 import 한다.

src/app/_component/MSWComponent.tsx
Pasted image 20240114203438.png|300
src/app/layout.tsx
Pasted image 20231217214953.png|500
Pasted image 20231217220013.png
if (typeof window !== "undefined") 로 브라우저에서만 돌아간다는 것을 보장하기 위해 한 번 걸러준다.

2-2) 클라이언트 환경에서는

public/mockServiceWorker.js 가 가로채서 http.ts 서버로 보내버린다. 그리고 handlers.js가 실행 됨.

2-3) .env.env.local

.env // at root, 배포시에는 .env가 동작함
.env.local // at root, 개발시에 .env.local 동작
  • 실은 개발 시에 .env, .env.local 모두 실행된다. .env.local 하나를 더 실행시키는 것과 동일하다.
  • 배포 시, .env만 실행

.env.local

NEXT_PUBLIC_API_MOCKING=enabled // 브라우저 접근 가능
NEXT_PUBLIC_BASE_URL=http://localhost:9090 
AUTH_URL=http://localhost:9090 // 서버에서만 접근 가능, 브라우저 접근 불가능
  • NEXT_PUBLIC 프리픽스를 붙이면 브라우저에서 접근 가능한 변수가 된다.
    Pasted image 20240114204644.png|300
    즉 이 파일들에서 모두 접근이 가능하다는 뜻!

  • 서버에서만 사용하려면 프리픽스를 빼주면 된다.
    중요한 것들은 NEXT_PUBLIC 프리픽스를 제외하여 설정하도록 한다.

그러면 개발 할 때에만 mocking이 실행 됨.
MSWComponent.tsx에서 설정해둔 값을 확인해보면 알 수 있음

3. 서버 컴포넌트에서 Server Actions 사용하기

3-1) 회원가입과 로그인 구현하기

3-1-1) Server Actions 이란? (Next 14)

(beforeLogin)/_component/SignupModal.tsx

기존의 클라이언트 컴포넌트를 서버 컴포넌트로 바꾼다.

  1. ‘use client’ 삭제
  2. state 모두 삭제
    input 태그에 require, name 속성(attribute)을 추가해준다.
    : name은 나중에 value에 접근하는데에 사용된다.
    FormData 를 사용하면 useState가 필요 없다.
  3. 온클릭 등 이벤트 모두 삭제

3-1-2) formData 적용

  1. 마크업에서는 form action 과 name 속성값을 추가한다.
    Pasted image 20231217225448.png|500

  2. submit 함수는 아래와 같이 설정한다.
    특히, try-catch문 안에서는 redirect 해주면 안 되고, 플래그 변수를 하나 둬서 try-catch 밖에서 진행하게 해줌.
    이건 서버 redirect만 해당한다!

아래 이미지는 서버코드이다.
Pasted image 20231217225530.png|600
여기까지가 서버 컴포넌트에서 Server Actions 사용하기였다.

아래는 코드 전문이다.

  1. 클라이언트 컴포넌트로 state와 이벤트들이 있다.
"use client";

import style from '@/app/(beforeLogin)/_component/login.module.css';
import {useState} from "react";

export default function LoginModal() {
  const [id, setId] = useState();
  const [password, setPassword] = useState();
  const [message, setMessage] = useState();
  const onSubmit = () => {};
  const onClickClose = () => {};

  const onChangeId = () => {};

  const onChangePassword = () => {};

  return (
    <div className={style.modalBackground}>
      <div className={style.modal}>
        <div className={style.modalHeader}>
          <button className={style.closeButton} onClick={onClickClose}>
            <svg width={24} viewBox="0 0 24 24" aria-hidden="true"
                 className="r-18jsvk2 r-4qtqp9 r-yyyyoo r-z80fyv r-dnmrzs r-bnwqim r-1plcrui r-lrvibr r-19wmn03">
              <g>
                <path
                  d="M10.59 12L4.54 5.96l1.42-1.42L12 10.59l6.04-6.05 1.42 1.42L13.41 12l6.05 6.04-1.42 1.42L12 13.41l-6.04 6.05-1.42-1.42L10.59 12z"></path>
              </g>
            </svg>
          </button>
          <div>로그인하세요.</div>
        </div>
        <form onSubmit={onSubmit}>
          <div className={style.modalBody}>
            <div className={style.inputDiv}>
              <label className={style.inputLabel} htmlFor="id">아이디</label>
              <input id="id" className={style.input} value={id} onChange={onChangeId} type="text" placeholder=""/>
            </div>
            <div className={style.inputDiv}>
              <label className={style.inputLabel} htmlFor="password">비밀번호</label>
              <input id="password" className={style.input} value={password} onChange={onChangePassword} type="password" placeholder=""/>
            </div>
          </div>
          <div className={style.message}>{message}</div>
          <div className={style.modalFooter}>
            <button className={style.actionButton} disabled={!id && !password}>로그인하기</button>
          </div>
        </form>
      </div>
    </div>
  );
}
  1. 이 코드를 서버 컴포넌트로 바꾸어서 ServerActions을 사용한다.
    1. state, 이벤트 모두 삭제
    2. 뒤로가기 버튼의 경우 -> 클라이언트 컴포넌트로 분리한다.
    3. input 태그에 name 속성을 지정한다. formData를 그대로 받기 위해서다.
    4. required 속성 추가해준다.
    5. form action={onSubmit} -> onSubmit이 ServerActions이 된다.
import BackButton from "./BackButton";
import { redirect } from "next/navigation";
import style from "@/app/(beforeLogin)/_component/login.module.css";

export default function LoginModal() {
  const onSubmit = async (formData: FormData) => {
    // formData에는 input의 name으로 접근하여 값을 가져올 수 있다. formData.get('id')

    "use server"; // 클라이언트에서는 보이지 않는다.
    let shouldRedirect = false;
    try {
      const response = await fetch("msw에서설정한가짜api주소넣어주면된다!", {
        method: "post",
        body: formData,
        credentials: "include", // 쿠키 전달 옵션.
      });
      
	  // 403 같은 경우는 백엔드와 협의하여 정한다.
      if (response.status === 403) {
        return { message: "user_exists" }; // 이미 계정이 있다는 메시지 반환
      }
      shouldRedirect = true;
    } catch (error) {
      console.error(error);
      return;
    }
    
	// 에러 발생 시에도 redirect 실행되지 않게끔 flag 변수로 분기처리한다.
    if (shouldRedirect) {
      redirect("/home"); // 리다이렉트는 절대 try-catch 내부에서 작성하지 않는다.
    }
  };

  return (
    <div className={style.modalBackground}>
      <div className={style.modal}>
        <div className={style.modalHeader}>
          <BackButton />
          <div>로그인하세요.</div>
        </div>
        <form action={onSubmit}>
          {" "}
          {/* onSubmit 이벤트를 action으로 바꿨다. */}
          <div className={style.modalBody}>
            <div className={style.inputDiv}>
              <label className={style.inputLabel} htmlFor="id">
                아이디
              </label>
              <input
                id="id"
                name="id" // formData는 name을 이용하여 가져올 수 있다.
                className={style.input}
                type="text"
                placeholder=""
                required // required를 해야 아래 버튼이 활성화 되지 않을 것임
              />
            </div>
            <div className={style.inputDiv}>
              <label className={style.inputLabel} htmlFor="password">
                비밀번호
              </label>
              <input
                id="password"
                name="password"
                className={style.input}
                type="password"
                placeholder=""
                required
              />
            </div>
          </div>
          <div className={style.message}>{message}</div>
          <div className={style.modalFooter}>
            <button type="submit" className={style.actionButton}>
              로그인하기
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

4. 클라이언트 컴포넌트에서 Server Actions 사용하기

다시 클라이언트 컴포넌트에서 ServerActions을 사용해보자.

  1. onSubmit 함수를 _lib 같은 폴더로 분리해주자. // _lib/signup.ts
  2. useFormState, useFormStatus - 리액트에서 제공해주는 함수들
    1. const [state, formData] = useFormState();
    2. const {pending} = useFormStatus();
// _component/SignupModal.tsx

"use client";

import style from './signup.module.css';
import onSubmit from '../_lib/signup'; // onSubmit 함수를 분리했음!
import BackButton from "@/app/(beforeLogin)/_component/BackButton";
import { useFormState, useFormStatus } from 'react-dom';

function showMessage(messasge: string) {
  if (messasge === 'no_id') {
    return '아이디를 입력하세요.';
  }
  if (messasge === 'no_name') {
    return '닉네임을 입력하세요.';
  }
  if (messasge === 'no_password') {
    return '비밀번호를 입력하세요.';
  }
  if (messasge === 'no_image') {
    return '이미지를 업로드하세요.';
  }
  if (messasge === 'user_exists') {
    return '이미 사용 중인 아이디입니다.';
  }
  return '';
}

export default function SignupModal() {
  const [state, formAction] = useFormState(onSubmit, { message: null });
  const { pending } = useFormStatus();

  return (
    <>
      <div className={style.modalBackground}>
        <div className={style.modal}>
          <div className={style.modalHeader}>
            <BackButton />
            <div>계정을 생성하세요.</div>
          </div>
          <form action={formAction}>
            <div className={style.modalBody}>
              <div className={style.inputDiv}>
                <label className={style.inputLabel} htmlFor="id">아이디</label>
                <input id="id" name="id" 
	                className={style.input} 
	                type="text" placeholder=""
                    required
                />
              </div>
              <div className={style.inputDiv}>
                <label className={style.inputLabel} htmlFor="name">닉네임</label>
                <input id="name" name="name" 
	                className={style.input} 
	                type="text" placeholder="" 
	                required
                />
              </div>
              <div className={style.inputDiv}>
                <label className={style.inputLabel} htmlFor="password">비밀번호</label>
	                <input id="password" name="password" 
		                className={style.input} 
		                type="password" placeholder="" 
		                required
                />
              </div>
              <div className={style.inputDiv}>
                <label className={style.inputLabel} htmlFor="image">프로필</label>
                <input id="image" name="image" 
	                className={style.input} type="file" 
	                accept="image/*"
	                required 
                />
              </div>
            </div>
            <div className={style.modalFooter}>
              <button type="submit" 
	              className={style.actionButton} 
	              disabled={pending}>
	              가입하기
			  </button>
              <div className={style.error}>{showMessage(state?.message)}</div>
            </div>
          </form>
        </div>
      </div>
    </>)
}
// _lib/signup.ts

"use server";

import { redirect } from "next/navigation";
import { signIn } from "@/auth"; // 서버라서 auth

export default async (prevState: any, formData: FormData) => {
  if (!formData.get("id") || !(formData.get("id") as string)?.trim()) {
    return { message: "no_id" };
  }
  if (!formData.get("name") || !(formData.get("name") as string)?.trim()) {
    return { message: "no_name" };
  }
  if (
    !formData.get("password") ||
    !(formData.get("password") as string)?.trim()
  ) {
    return { message: "no_password" };
  }
  if (!formData.get("image")) {
    return { message: "no_image" };
  }
  let shouldRedirect = false;
  try {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_BASE_URL}/api/users`,
      {
        method: "post",
        body: formData,
        credentials: "include",
      }
    );
    if (response.status === 403) {
      return { message: "user_exists" };
    }
    shouldRedirect = true;
    await signIn("credentials", {
      username: formData.get("id"),
      password: formData.get("password"),
      redirect: false,
    });
  } catch (err) {
    console.error(err);
    return;
  }

  if (shouldRedirect) {
    redirect("/home"); // try-catch문 안에서 X
  }
};

다국어 대응할 때…

한글 보다도 영어로 사용하는 것이 다국어 대응할 때 더 좋다.
Pasted image 20231218163824.png

form 과 관련된 React hook

form에서 state를 사용할 수 있는 훅과 - useFormState
form에 데이터를 보내면 서버쪽으로 보낸 데이터의 상태를 알 수 있다. - useFormStatus

const [state, formAction] = useFormState(onSubmit, { message: "" }); // form submit함수와 초기값
const { pending } = useFormStatus();

 return (
    <>
	  <form action={formAction}>
      ...
      </form>
    </>

ts 단언

아래의 경우 image일 땐 string이 아닐 수도 있으므로 ts에서 as로 단언해준다.
or !로
Pasted image 20240115222332.png

5. Auth.js, 미들웨어 API 라우트, catch-all 라우트

app 폴더와 같은 레벨에 auth.ts, middleware.ts 작성
Pasted image 20240115222648.png

npm i next-auth@5 @auth/core

Middleware

Next의 Middleware를 통해 페이지 별 접근할 수 있는 라우트를 설정할 수 있다.
페이지별 접근 권한을 설정하는 것과 비슷하다.

catch-all route

Pasted image 20240115234909.png

(혹시 틀린 정보가 있다면 댓글로 알려주세요!)

  • 이 글은 옵시디언에서 작성되었습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함