티스토리 뷰
1. MSW 세팅과 버전 업그레이드
1. MSW로 미리 API 만들기 - 서버 쪽 세팅
유의. 넥스트에서 매끄럽게 되지 않는 부분이 있다.
1-1) 설치
npx msw init public/ --save // 넥스트 프로젝트 폴더 내에 public 폴더
// y 클릭
npm install msw --save-dev
(1번을 실행하면 public에 mockServiceWorder.js가 실행됨)
🧐 어떤 때에 MSW를 쓰나요?
개발 환경에서 억지로 에러를 내거나 매번 로그인하는 것을 skip할 때 msw를 사용하면 좋다.
1-2) src/mocks 폴더구조
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 명령어 설정해두고, 서버 띄워서 실행시켜두면 편하다.
🤝 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
src/app/layout.tsx
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 프리픽스를 붙이면 브라우저에서 접근 가능한 변수가 된다.
즉 이 파일들에서 모두 접근이 가능하다는 뜻! -
서버에서만 사용하려면 프리픽스를 빼주면 된다.
중요한 것들은 NEXT_PUBLIC 프리픽스를 제외하여 설정하도록 한다.
그러면 개발 할 때에만 mocking이 실행 됨.
MSWComponent.tsx에서 설정해둔 값을 확인해보면 알 수 있음
3. 서버 컴포넌트에서 Server Actions 사용하기
3-1) 회원가입과 로그인 구현하기
3-1-1) Server Actions 이란? (Next 14)
(beforeLogin)/_component/SignupModal.tsx
기존의 클라이언트 컴포넌트를 서버 컴포넌트로 바꾼다.
- ‘use client’ 삭제
- state 모두 삭제
input 태그에 require, name 속성(attribute)을 추가해준다.
: name은 나중에 value에 접근하는데에 사용된다.
FormData 를 사용하면 useState가 필요 없다. - 온클릭 등 이벤트 모두 삭제
3-1-2) formData 적용
-
마크업에서는 form action 과 name 속성값을 추가한다.
-
submit 함수는 아래와 같이 설정한다.
특히, try-catch문 안에서는 redirect 해주면 안 되고, 플래그 변수를 하나 둬서 try-catch 밖에서 진행하게 해줌.
이건 서버 redirect만 해당한다!
아래 이미지는 서버코드이다.
여기까지가 서버 컴포넌트에서 Server Actions 사용하기였다.
아래는 코드 전문이다.
- 클라이언트 컴포넌트로 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>
);
}
- 이 코드를 서버 컴포넌트로 바꾸어서
ServerActions을 사용한다.- state, 이벤트 모두 삭제
- 뒤로가기 버튼의 경우 -> 클라이언트 컴포넌트로 분리한다.
input태그에name속성을 지정한다.formData를 그대로 받기 위해서다.required속성 추가해준다.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을 사용해보자.
- onSubmit 함수를
_lib같은 폴더로 분리해주자. //_lib/signup.ts - useFormState, useFormStatus - 리액트에서 제공해주는 함수들
- const [state, formData] = useFormState();
- 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
}
};
다국어 대응할 때…
한글 보다도 영어로 사용하는 것이 다국어 대응할 때 더 좋다.
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 !로
5. Auth.js, 미들웨어 API 라우트, catch-all 라우트
app 폴더와 같은 레벨에 auth.ts, middleware.ts 작성
npm i next-auth@5 @auth/core
Middleware
Next의 Middleware를 통해 페이지 별 접근할 수 있는 라우트를 설정할 수 있다.
페이지별 접근 권한을 설정하는 것과 비슷하다.
catch-all route
(혹시 틀린 정보가 있다면 댓글로 알려주세요!)
- 이 글은 옵시디언에서 작성되었습니다.
'[개발] > Next' 카테고리의 다른 글
| [Next] 섹션3 그런데 백엔드 개발자가 API를 아직 못 만들었다 (2) (0) | 2023.12.18 |
|---|---|
| [Next] 섹션2 본격 클론 시작 (작성 중) (0) | 2023.12.16 |
| [Next] 섹션1 기획자와 디자이너가 기획서를 던져주었다. (0) | 2023.12.14 |
| [Next] 페러렐 라우트 (parallel-routes)와 인터셉팅 라우트 (intercepting-routes) (1) | 2023.12.14 |
| [Next] React 18 - Automatic Batching (0) | 2023.12.13 |