This is a Next.js project bootstrapped
with create-next-app
.
- νμκ°μ , λ‘κ·ΈμΈ
- Point μΉ΄λ λ±λ‘, μ‘°ν
- Point μ 립, μ¬μ© λ΄μ νμΈ, μ‘°ν
- Point μ¬μ©κ°λ₯μ² μ‘°ν (Map)
- νμ κ΄λ¦¬(λ±λ‘, μμ , κΈ°λ³Έμ 보, ν¬μΈνΈ λ΄μ)
- Point μΉ΄λ κ΄λ¦¬(λ±λ‘, μμ )
- μ¬μ©κ°λ₯μ² λ±λ‘(Map)
- μ μ κ° μΉ΄λ λ±λ‘μ μ μ μ 보(userEmail, userName, userId)κ° κΈ°λ³Έμ μΌλ‘ κ°μ΄ λ±λ‘λ¨.
- λ‘κ·ΈμΈ session μ 보 μ€ user._id κ°μΌλ‘ μΉ΄λμ userId κ°μ νΉμ νμ¬ μμ μ΄ λ±λ‘ν μΉ΄λλ₯Ό νΉμ ν μ μμ.
- λ°±μλ apiμμλ νμ¬ sessionμ user._id κ°μ userId (cardModel schema)μ λμ ν΄μ dbλ‘μ 쿼리 μμ²μ νκ² λ¨.
const cards = await Card.find({ userId: session?.user._id });
- μ¬κΈ°μ session μ 보λ₯Ό 맀μΉνμ§ μκ³
Card.find({ })
λΉ κ°μΌλ‘ 쿼리νλ©΄ λͺ¨λ μΉ΄λμ λ³΄κ° νΈμΆ λλ€. - μλ²μ¬μ΄λ μμ νμ¬ session μ λ³΄κ° μμλ dbλ₯Ό μ‘°νν΄μ userId κ°μ κ°μ ΈμμΌ ν κ²μ΄λ€.
- api/mypage/route.ts μμ±
- db Schema λ cardModel μ κ·Έλλ‘ μ¬μ©.
import {useSession} from "next-auth/react";
const {data: session, status} = useSession();
try {
await connectDB();
const cards = await Card.find({ userId: session?.user._id });
return NextResponse.json(cards, { status: 200 });
}
- κ²°κ³Όλ
Internal Server Error 500
μ€ν¨ - apiμ endpoint λ λ¬Έμ κ° μκ³ , μλ§λ session μ κ°μ Έμ€μ§ λͺ»νκ±°λ user._id λ₯Ό κ°μ Έμ€μ§ λͺ»νμ¬ DB μΏΌλ¦¬κ° μλκ²μΌλ―?
Card.find({ })
μ κ°μ΄ λΉκ°μΌλ‘ νΈμΆν΄λ΄λ μμ 500μλ¬. session μ μ·¨λνλ μ½λμ λ¬Έμ κ° μλλ― νλ€. session μ½λλ₯Ό μ§μ°κ³ λͺ¨λ κ° νΈμΆμ λͺ¨λ μΉ΄λ μ λ³΄κ° νΈμΆλ¨.
- https://next-auth.js.org/tutorials/securing-pages-and-api-routes
- nextauth 곡μμ¬μ΄νΈμ μλ²μ¬μ΄λμμμ μΈμ μ·¨λ κ΄λ ¨κΈμ΄ μλ€.
- 1μ°¨ μλ λ°©λ²μ νλ‘ νΈμμμ μ μ©λ°©λ² μ΄μμ.
getServerSession
λ©μλλ₯Ό μ΄μ©ν΄μ api κ²½λ‘λ₯Ό 보νΈν μ μλ€λΌλ λ΄μ©.
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/libs/auth";
const session = await getServerSession(req, res, authOptions);
if(session) {
const cards = await Card.find({ userId: session?.user._id });
return NextResponse.json(
cards,
{ status: 200 }
);
} else {
res.status(401);
}
- κ²°κ³Όλ
Internal Server Error 500
μ€ν¨
- session μ user μ 보 μ€ _id κ°μ΄ νμνλ€.
- μΉ΄λλ±λ‘μ μν μΉ΄λλͺ¨λΈ, API λΌμ°ν°, λ±λ‘ form νλ©΄ μΆκ°
- μ λ ₯λ°λ μΉ΄λμ 보λ μΉ΄λλ²νΈ, CVC
- mongoDB μ μ μ₯λ db ꡬ쑰λ₯Ό cardModel.ts μ μ μνλ€.
- μ¬κΈ°μ userEmail, userName μ μΈμ¦ νμ session μ 보μμ μ°Έμ‘°νκ² λλ€.
("Card", cardSchema)
λ‘ μ μλμ΄ mongoDB μcards
λΌλ Collection μ μ μ₯λ κ²μ΄λ€.- TODO: νμ¬λ
test
λΌλ db μ μ μ₯λκ³ μλλ° db λ₯Ό μ§μ νμ¬(e.g. db μ΄λ¦μ΄ pointapp) μ μ₯νλ λ°©λ²λ νμΈ - userλ nκ°μ μΉ΄λλ₯Ό λ±λ‘ν μ μκΈ°μ μΉ΄λκ΄λ¦¬λ₯Ό μν΄ μΉ΄λμ λ§€μΉ λ μ μ μ unique ν μ λ³΄κ° λ±λ‘λμ΄μΌ νκ³ μ΄ μ λ³΄λ‘ μΉ΄λλ₯Ό κ²μ¦ν΄μΌ ν κ²μ΄λ€.
- user μ unique μ 보λ email κ³Ό _id κ°μ΄ μμ.
// cardModel.ts
const cardSchema: Schema = new Schema({
cardNumber: {
type: Number,
unique: true,
required: [true, "Card number is required"],
},
cvc: {
type: Number,
unique: false,
required: [true, "CVC number is required"],
},
userEmail: {
type: String,
required: true
},
userName: {
type: String,
required: true
},
},
{
timestamps: true,
}
);
const Card: Model<PointCard> = mongoose.models.Card || mongoose.model<PointCard>("Card", cardSchema);
- μΉ΄λμμ± νλ©΄μμ μΉ΄λ μ μ² λ²νΌ ν΄λ¦μ Random μΉ΄λμΌλ ¨μ CVC(Card Verification Code) λ²νΈ μμ±
- μμ±λ μΉ΄λμ 보λ₯Ό λ°μ΄ν°λ² μ΄μ€μ μ μ₯.
- μ€λ¬ΌμΉ΄λκ° μλ λμ§νΈμΉ΄λ λ±λ‘ (μΉ΄λλ²νΈ + CVC)
- google, github λ±μ OAuth μ€μ μ΄ μκ°λ³΄λ€ μ½λ€.
- λ‘컬 envμ GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET μ κ°μ μ€μ μ μΆκ°νκ³ , ν΄λΉ μ½μνλ©΄(Configuration)μμ Callback URI λ₯Ό μΆκ°ν΄ μ£Όλ©΄ λλ€.
- OAuth μ 곡 νμ΄μ§ μμ μμΈν μ€μ μ μ°Έκ³ .
- μΈμ¦μ μν callback URI λ /api/auth/callback/google μ΄λ°μμ΄λ€.
- production λλ©μΈκ³Ό localhostλ₯Ό κ°μ΄ μΆκ°ν μ μλ κ²½μ°λ λ‘컬 νκ²½μμ production λλ©μΈμΌλ‘ λμ΄κ° μ μλ€.
- μ΄ λ, λ°°ν¬λλ μλ²μ λ°°ν¬ νκ²½μ€μ λ κ°μ΄ μ€μ ν΄ μ€μΌ νλ€.
- Vercel μμ λ°°ν¬μ Enviroment μ μ©λλ₯Ό μ€μ ν μ μλ€. (Production, Development)
- Nextauth (νμ¬ 4.x) κ° 5.x λ²μ μΌλ‘ μ λ°μ΄νΈ λλ©΄μ μλΉμ€ λͺ μΉμ΄ Auth.js μΌλ‘ λ°λμλ€. μμ§κΉμ§λ 5λ²μ μΌλ‘ λ§μ΄κ·Έλ μ΄μ νμ§ μμ μν.
- μΈμ¦μ νκ³ session μ΄ ν리λ νμμ΄ μμ. (μλ‘κ³ μΉ¨ λ±)
- session λλ μ΄: μΈμ¦ session μ·¨λ ν next/link λ₯Ό ν΅ν μΈν°λ νμ΄μ§ μ΄λμμλ 보μ΄μ§ μμΌλ μλ‘κ³ μΉ¨μ νμ κ²½μ° session μ·¨λ μ΄μ μ UI (Sign In, Log In λ²νΌ λ±) μ΄ μ½ 1~2μ΄ μ λ λ³΄μΈ ν session μ΄ μ μ©λ UI (μ μ λ€μ, Log Out)κ° λ³΄μ΄λ νμμ΄ μμ.
- SSR μ΄μμΈκ°? Nextauthμ λ²κ·ΈμΈκ°? 5.x λ‘ μ λ°μ΄νΈ νλ©΄ ν΄κ²°λλ κ²μΈμ§? νμ¬λ‘μ¨λ μ’ λ νμΈν΄ λ΄μΌ ν λ¬Έμ κ°λ€.
- Auth.js μμ session prefetch κΈ°λ₯μ΄ μλ€. Roor μ»΄ν¬λνΈ Provider μμ session μ΄λ props λ₯Ό ν΅ν΄ route μ΄λμμ 미리 session μ 보λ₯Ό κ°μ§κ³ μλκ°λ κ°λ κ°μλ°, SessionProvider μμ μ΄ λΆλΆμ΄ μ΄λ―Έ μ μ©λμ΄ μλκ±° μλμλ? μ΄ λΆλΆμ μ’ λ νμΈνμν¨.
- μΌλ¨μ Session status μ Loading μνλ‘ μ€κ°μ μκΈ°λ λλ μ΄ μ€μ ν΄λΉ UI λΆλΆμ μ€μΌλ ν€ UIλ₯Ό μ μ©ν΄μ λ‘λ© UIλ₯Ό ννν¨.
// Session status 'Loading' μΌ λ λ³΄μ¬ μ€ UI κ°μ²΄
const loadingAnimation = keyframes`
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
`;
const SkeletonSpan = styled.span < {width: string, height: string, margin: string} > `
display: block;
background: linear-gradient(90deg, #DADADA, #f5f5f5, #DADADA);
background-size: 200% 100%;
animation: ${loadingAnimation} 1.5s infinite;
width: ${props => props.width};
height: ${props => props.height};
margin: ${props => props.margin};
border-radius: 8px;
`;
// Header.tsx
const showSession = () => {
if (status === "authenticated") {
return (
<>
...
</>
)
} else if (status === "loading") {
return (
<>
<SkeletonSpan width="68px" height="32px" margin="0 0 0 16px"/>
<SkeletonSpan width="64px" height="32px" margin="0 0 0 16px"/>
</>
)
} else {
return (
<>
...
</>
)
}
}
- μ¬μ©μμΈμ¦μ NextAuth.js λ‘ μ μ©νκΈ°λ‘ ν¨.
- 곡μμ¬μ΄νΈμμλ Next.js μ μν μμ ν μ€ν μμ€λ‘ μκ°νκ³ μλ€.
- μΈμ¦μ ꡬννλ resource λ€μ΄ μμ 리λμ΄ μλ€. Get Started
- NextAuth.js λ
/api/auth
path μμμ μΈμ¦μ μ²λ¦¬νλ€. - /app/api/auth/[...nextauth]/route.ts λΌμ°νΈλ₯Ό λ§λ€μ΄ GET, POST method λ₯Ό 맀ννλ handler λ₯Ό λ§λ€μ΄ μ€λ€. (Next.js 14 κΈ°μ€)
- [...] μ λμ λΌμ°ν μ μλ―Έν¨. signIn, signOut, callback λ±μ μμ²μ΄ μ¬κΈ°λ₯Ό ν΅ν΄ μ²λ¦¬λλ€. auth.ts λ₯Ό μ°κ²°ν΄μ μΈμ¦λΆλΆκΉμ§
All requests to /api/auth/*(signIn, callback, signOut, etc.) will automatically be handled by NextAuth.js.
- /api/auth/* λ‘ λ€μ΄μ€λ api μμ²μ /api/auth/[...nextauth]/route.ts μμ μλμΌλ‘ μ²λ¦¬ν΄ μ€λ€λ κ²μ΄λ€.
// app/api/auth/[...nextauth]/route.ts
import {authOptions} from "@/libs/auth";
import NextAuth from "next-auth";
const handler = NextAuth(authOptions);
export {
handler as GET, handler as POST
}
- μΈμ¦μ μ²λ¦¬νλ auth.ts λ₯Ό μμ±νλ€. λλ
/app/lib/auth.ts
λ‘ μμ±
// /app/lib/auth.ts
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
CredentialsProvider({
name: "Credentials",
id: "credentials",
credentials: {
email: {label: "Email", type: "text", placeholder: "Email"},
password: {label: "Password", type: "password"},
},
async authorize(credentials, req): Promise<any> {
await connectDB();
const userFound = await User.findOne({
email: credentials?.email,
}).select("+password");
if (!userFound) throw new Error("Invalid Email");
const passwordMatch = await bcrypt.compare(
credentials
!
.
password,
userFound.password,
)
;
if (!passwordMatch) throw new Error("Invalid Password");
return userFound;
},
}),
],
// μλ΅
- κΈ°λ³Έμ μΌλ‘ email κ³Ό λΉλ°λ²νΈ μΈμ¦μ μ 곡νλ€. κ΅¬κΈ OAuth λ μ 곡νλ€.
- provider λ‘ κ΅¬κΈ, κΉνλΈ κ°μ OAuth μΈμ¦, κΈ°λ³Έμ μΈ credentials μ κ°μ μ격μ¦λͺ μΈμ¦(email λλ ID μ λΉλ°λ²νΈ)μ μ 곡νλ€.
CredentiaslProvider
μμλcredentials
νλ‘νΌν°λ₯Ό ν΅ν΄ μΈμ¦μ μν΄ μ΄λ€ μ 보λ₯Ό μ λ ₯λ°μμ§ μ§μ ν μ μλ€. (email or user id)- μμμ μ λ ₯λ°μ νλ‘νΌν° κ°μ κ°μ§ κ°μ²΄κ° authorize μ½λ°± ν¨μμ 첫λ²μ§Έ μΈμλ‘ μ λ¬λ¨(HTTP POST) κ·Έλ¬λ©΄ μ¬μ©μκ°μ²΄(email, name, _id λ±), λλ null, Error μ€ νλλ₯Ό λ°ννλ κ²μ΄λ€.
- μ¬μ©μ λ±λ‘μ μν λ‘μ§μ ꡬννλ€. νλ‘ νΈμμ μ λ ₯λ°μ λ°μ΄ν°λ₯Ό λ°±μλλ‘ λ³΄λ΄κ³ λ°±μλμμλ ν΄λΉ λ°μ΄ν°μ λͺ¨λΈμ ν΅ν΄ κ²μ¦ν ν dbμ μ μ -> μ μ₯νκ² λλ€.
- TODO: μ¬μ©μ μ΄λ©μΌ μ€λ³΅μ²΄ν¬, λ°μ΄ν° λͺ¨λΈ μ μκ° νμνλ€.
- bcryptλ‘ passwordλ₯Ό μνΈν νλ€.
export async function POST(request: NextRequest) {
try {
const client = await connect;
const body = await request.json();
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(body.password, salt);
await client.db("test").collection("users").insertOne({
name: body.name,
email: body.email,
password: hashedPassword
});
return NextResponse.json({
message: "successfully updated the document",
success: true,
})
} catch (error: any) {
return NextResponse.json({error: error.message}, {status: 500})
}
}
- onSubmit μ΄λ²€νΈλ‘ νΌμ μ μ‘νκ³ ,
event.preventDefault()
λ‘ νΌμ΄ 무μμ μ μ‘λλκ±Έ λ§λλ€. - μ λ ₯κ° validator νμν¨.
- μΈμ¦ λ‘μ§μ΄ μμ±λλ©΄ μΈμ¦μ¬λΆμ λ°λ₯Έ μ κ·Ό μ¬λΆ μμ λ νμ(Router).
const [email, setEmail] = useState < string > ("");
const [name, setName] = useState < string > ("");
const [password, setPassword] = useState < string > ("");
const userInput = {
email,
name,
password,
}
const submitHandler = async (event) => {
event.preventDefault();
try {
const {data, status} = await axios.post("/api/users/regist", userInput, {
headers: {
'Content-Type': 'application/json'
}
});
if (status === 200) {
router.push("/member/login")
}
} catch (error) {
console.log(error.message)
}
}
<form onSubmit={submitHandler}>
<InputGroup
inputId="email"
type="email"
label="μ΄λ©μΌ"
title="Email"
required={true}
value={email}
placeholder="Email"
className="input-text"
onchange={(e) => setEmail(e.target.value)}
/>
.
.
.
<Input
type="submit"
value="Submit"
className="input-button submit"
/>
</form>
μΌλ¨ λ±λ‘μ μ μμ μΌλ‘ λλ€.
- bcrypt, jsonwebtoken ν¨ν€μ§λ‘ μ μ λ±λ‘μ 보λ΄λ λ°μ΄ν°λ₯Ό μνΈννκ³ μΉν ν°μ λ§λ€μ΄μ μ μ μ λ°μ΄ν°λ₯Ό κ²μ¦νκ³ μΈμ¦ν κ² μ΄λ€.
Nextjs App Router μμλ μ½μλ νμΌ μ€(layout, page, route λ±λ±) νλμΈ loading.ts λΌλ νμΌμ΄ μλ€. μ΄λ νμ΄μ§κ° μ΄λ μ λ λλ§μ΄ λλ λμ λ체λλ ui λ₯Ό 보μ¬μ€ μ
μλ€.
μ»΄ν¬λνΈλ³λ‘λ μ μ©ν μ μλ€.
layout.tsxμ page.tsx μ κ°μ κ²½λ‘μ λ§λ€μ΄ λλ©΄ ν΄λΉ layout.tsx λ‘ λ©νλμ΄ μλ νλ©΄ λ° μ»΄ν¬λνΈλ λͺ¨λ μλ μ μ©λλ€.
Suspense
λ₯Ό ν΅ν΄ μ»΄ν¬λνΈ λ³λ‘ λ‘λ© UIλ₯Ό μλ μ€μ ν μ μλ€.
- loading.tsx νμΌμ μ΅μλ¨μ page.tsx μ κ°μ κ²½λ‘μ λμ΄ κ³΅ν΅ λ‘λ© UIλ‘ μ μ©ν¨. νμ΄μ§κ° μ΄λμμ μ μ©λλ κ²μ΄ νμΈλ¨. (νλ‘μ νΈ μ΄κΈ° λ¨κ³λΌ νμ΄μ§ λ λλ§μ΄ λΉ¨λΌμ μΈμ§ κ±°μ 보μ΄μ§ μμ)
<Suspense><Component /></Suspense>
μ¬μ©ν΄ 보μμΌλ μ μ©μ΄ μλλκ±° κ°μ... μ΄μ λ νμ€ν λͺ¨λ₯΄κ² λ€. react developer tools λ‘ λ§μ΄νΈ λ components λ₯Ό 보면<Suspense>
κ° μ‘΄μ¬ν¨.- loading.tsc λ₯Ό μ¬μ©ν΄μ λ°μ΄ν° ν¨μΉ μνμ λ°λ₯Έ λ‘μ§μ ꡬν.
// loading μν μΆκ°
const [loading, setLoading] = useState<boolean>(true);
const getUsersHandler = async () => {
try {
const res = await axios.get("/api/users/");
if (res.status === 200) {
setUsers(res.data);
setLoading(false); // data κ° μ±κ³΅μ μΌλ‘ ν¨μΉλλ©΄ λ‘λ© κ°μΆ€
}
} catch (error) {
setError(true);
setLoading(false);
setErrMsg(
error.reponse && error.response.data.message
? error.response.data.message
: error.message
);
console.log(error.message);
}
}
if (loading) return <Loading / >;
return (
<>
...
</>
)
μ μ₯λ μ μ λ€ μ 보 κ°μ Έμ€κΈ°
users collection μ μ μ₯λ μ λ€μ λ°μ΄ν°(document)λ₯Ό front νλ©΄μμ νΈμΆνκΈ°
- app/api/users/route.ts μ μ μ λ°μ΄ν°λ₯Ό νΈμΆνλ μ½νΈλ‘€λ¬λ₯Ό λ§λ€μ΄ /api/users API endpoint μμ±
// app/api/users/route.ts
// /api/users
export async function GET(req: NextRequest) {
const client = await connect;
const usersData = await client.db("test").collection("users").find();
const getUsers = await usersData.toArray();
return NextResponse.json(getUsers);
}
// app/member/users/page.tsx
// /member/users
const [users, setUsers] = useState([]);
const getUsersHandler = async () => {
try {
const {data, status} = await axios.get("/api/users/");
if (status === 200) {
setUsers(data);
}
} catch (error) {
console.log(error.message);
}
}
useEffect(() => {
getUsersHandler();
}, []);
return (
<>
<ul>
{users && users.map((user) => (
<li key={user._id}>
{user.name}({user.email})
</li>
))}
</ul>
</>
)
DB μ μ₯ μ΄λ²€νΈ νΈλ€λ¬ μΆκ°
Backend μμ μ μ λ°μ΄ν° email, name μ users collection μ μ μ₯ν μ μλ μ΄λ²€νΈ νΈλ€λ¬λ₯Ό μΆκ°νλ€.
Front νλ©΄μλ email, name λ°μ΄ν°λ₯Ό λ³΄λΌ μ μλ form μ λ§λ λ€. (μ κ·μ μ λ±λ‘μ μν΄μλ password λ νμνλ€. password λ μνΈν λ‘μ§λ νμ)
Front μμλ μ μ λ°μ΄ν°λ₯Ό /api/users/regist api λ₯Ό νΈμΆνμ¬ dbμ μ μ₯νκ² λλ€.
// app/api/users/regist/route.ts
export async function POST(request: Request) {
const client = await connect;
const body = await request.json();
await client.db("test").collection("users").insertOne({
email: body.userEmail,
name: body.userName,
});
return Response.json({
message: "successfully updated the document"
})
}
// app/member/join/page.tsx
const submitHandler = () => {
fetch("/api/users/regist", {
method: "POST",
body: JSON.stringify({
userEmail,
userName,
userPassword
})
})
}
// <InputGroup> μ input κ³Ό label λ±μΌλ‘ ꡬμ±λ μ»΄ν¬λνΈ
<form onSubmit={submitHandler}>
<InputGroup
inputId="userEmail"
type="email"
title="Email"
required={true}
value={userEmail}
placeholder="Email"
onchange={(e) => setUserEmail(e.target.value)}
/>
<InputGroup
inputId="userName"
type="text"
title="Name"
required={true}
value={userName}
placeholder="Name"
onchange={(e) => setUserName(e.target.value)}
/>
<Input
type="submit"
value="Submit"
/>
</form>
MongDBμ μ°κ²°νκ³ λ°μ΄ν°λ₯Ό POST, GET νλ ν μ€νΈ μ½λ.
- API route μ κΈ°λ³Έμ½λ
const {MongoClient, ServerApiVersion} = require("mongodb");
// Replace the placeholder with your Atlas connection string
const uri = "<connection string>";
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
const client = new MongoClient(uri, {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
}
}
);
async function run() {
try {
// Connect the client to the server (optional starting in v4.7)
await client.connect();
// Send a ping to confirm a successful connection
await client.db("admin").command({ping: 1});
console.log("Pinged your deployment. You successfully connected to MongoDB!");
} finally {
// Ensures that the client will close when you finish/error
await client.close();
}
}
run().catch(console.dir);
dbμ μ μν ν λ°μ΄ν°λ₯Ό 보λ΄λ ν μ€νΈ νμ΄μ§λ₯Ό μΆκ°.
// api/users/regist/route.ts
// /api/users/regist entpointκ° μμ±λ¨
export async function POST(request: Request) {
const client = await connect;
const cursor = await client.db("test").collection("users").insertOne({
email: "vuenos@gmail.com",
name: "Jintae Kim",
Age: "22"
});
return Response.json({message: "successfully updated the document"})
}
// app/test/page.tsx
export default function TestApi() {
useEffect(() => {
fetch("/api/users/regist", {
method: "POST"
})
}, []);
return (
<h1>Some Content</h1>
)
}
μ΄λ¬λ©΄ http://localhost:3000/test μ μμ
MongoDB μ 'test' db μ 'users' collection μ email, name, age λ°μ΄ν°λ₯Ό μ μ₯ν¨.
insertOne
: λ¨μΌ document λ₯Ό collection μ μ μ₯insertMany
: λ€μ€ document λ₯Ό collection μ μ μ₯
[μ°Έκ³ ] https://www.mongodb.com/ko-kr/docs/manual/reference/method/db.collection.insertOne/
[μ°Έκ³ ]
- https://www.mongodb.com/ko-kr/docs/atlas/device-sdks/web/nextjs/
- https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/connect/#std-label-node-connect-to-mongodb
- https://ellertsmarik.medium.com/json-api-using-next-js-13-and-mongodb-f45e8e61b031
DBλ λͺ½κ³ λλΉλ₯Ό μ¬μ©νκΈ°λ‘ νλ€.
λ±λ‘, μ‘°ν μ κΈ°λ₯μ΄ λλΆλΆμ΄λΌ 무리 μμ΄ λ³΄μλ€.
db,collectionμ μμ±
Express λ‘ λ°±μλλ₯Ό ꡬμ±.νμλ
νλ‘ νΈμ λ³κ°λ‘ μλ²νΈμ€ν
μ ꡬμ±ν΄μΌ νλ€
vercel λ‘ deploy νμ¬ production μνλ‘ λ§λ€μ΄μ μ¨λΌμΈμΌλ‘ μ μν΄ λ³΄λ©΄ νλ‘ νΈλ μ μμ μΌλ‘ 보μ΄μ§λ§
API λ₯Ό νΈμΆνμλ ν΅μ μ΄ λμ§ μλλ€. λ°±μλ 리μμ€λ₯Ό λ³λλ‘ νΈμ€ν
νμ¬ νΈμΆν΄μΌ νλ κ²μ΄λ€.
- Next.js λ‘ κ°λ°νλλ° Express λ‘ λ°±μλλ₯Ό λ³λλ‘ κ΅¬μ±νκΈ° λ³΄λ€ Next.js μ Route Handlers λ‘ endpoint λ₯Ό λ§λ€λ©΄ λκ² λ€λ μκ°μ΄ λ€μλ€.
- route ꡬμ±μ νλ‘ νΈμ λμΌνλ€. /app/api/users/route.ts λ /api/users λΌλ endpoint λ₯Ό λ§λ€μ΄ μ€λ€.
- /app/api/users/route.ts μ νΈμΆμ λ°νλλ μ΄λ²€νΈ νΈλ€λ¬λ₯Ό μμ±ν΄ μ£Όλ©΄ λλ€.
- GET, POST λΌλ ν¨μλͺ μ΄ κ·Έλλ‘ κ·Έ μνμ μνν΄ μ€λ€.
// /app/api/users/route.ts
import {NextRequest, NextResponse} from "next/server";
export async function GET(req: NextRequest) {
return NextResponse.json({
name: "Jintae Kim",
email: "vuenos@gmail.com",
isAdmin: true,
})
}
// /app/page.tsx
const callDataHandler = async () => {
try {
const {data, status} = await axios.get(`${url}/test`);
if (status === 200) {
setUserData(data);
setCallStatus("suceess");
setMsg(`μλ²μμ μ μμ μΌλ‘ μλ΅ν¨`);
}
} catch (error: any) {
console.log(error.message)
setCallStatus("failed");
setMsg(`μλ² ν΅μ μ€ν¨`);
}
;
};
νλ‘ νΈμμμ κ²°κ³Όλ μλμ κ°λ€.
ν€λμ κ³΅ν΅ λ€λΉκ²μ΄μ
μμ νμ¬ λ©λ΄μ νμ±ν μ€νμΌ μ μ©
React μμλ NavLink λ₯Ό μ΄μ©νλ©΄ μ½κ² μ μ©μ΄ λμ§λ§ Next.js μμλ μ§μνμ§ μλλ€.
κ·Έλμ 컀μ€ν
μ ν΄μΌ νλ€.
- 1μ°¨ λ°©λ² : next/router μ useRouter λ₯Ό μ΄μ©ν΄μ νμ¬νμ΄μ§μ pathname μ νΉμ ν΄μ λΉκ΅νλ λ°©λ²
- λ¨Όμ λ©λ΄μ μ 보μ λν κ°μ²΄ λ°°μ΄μ λ§λ€μ΄ μ£Όκ³ ,
const menuData = [
{id: "menu01", title: `${userName}`, path: "/member/mypage"},
{id: "menu02", title: "Login", path: "/member/login"},
{id: "menu03", title: "Sign up", path: "/member/join"},
];
- path κ°κ³Ό νμ¬ pathname μ λΉκ΅νμ¬ νΉμ μ€νμΌμ μ μ©νλ€.
const router = useRouter();
{
menuData.map((menuItem) => (
<Link
href={menuItem.path}
className={`${menuItem.path === router.pathname ? "active" : ""}`}
>
{menuItem.title}
</Link>
))
}
κ·Έλ°λ° λ€μκ³Ό κ°μ μλ¬κ° λ¬λ€.
Error: NextRouter was not mounted.
Next.js 13 λΆν°λ next/router λμ next/navigation λ₯Ό μ¬μ©ν΄μΌ νλ€.
// refactor
import {usePathname} from "next/navigation";
const pathname = usePathname();
{
menuData.map((menuItem) => (
<Link
href={menuItem.path}
className={`${menuItem.path === pathname ? "active" : ""}`}
>
{menuItem.title}
</Link>
))
}
μ°Έκ³ : https://nextjs.org/docs/app/api-reference/functions/use-router
Next.js 13 λΆν° app/ λλ ν 리λ₯Ό κΈ°λ°μΌλ‘ λΌμ°ν
μ΄ κ΅¬μ±λλ€.
app/about μ΄λ dir λ₯Ό λ§λ€κ³ page.tsx λ₯Ό μμ±νλ©΄ /about μ΄λ route κ° κ΅¬μ±μ΄ λλ κ²μ΄λ€.
Layout.tsx λ‘ λ μ΄μμμ ꡬμ±ν μ μλ€. /app/page.tsx κ° "/" μ΄λ route κ° λλ€.
Layout μ μ¬λ¬ κ²½λ‘ κ°μ 곡μ λλ UI μ΄λ€. Route κ°μ μ΄λ μ λ μ΄μμ μνλ₯Ό μ μ§νκ³ μνΈκ΅ν μνλ₯Ό μ μ§νλ©° 리λ λλ§ λμ§ μλλ€.
// app/layout.js
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body>
<Header/>
<Wrapper>{children}</Wrapper>
<Footer/>
</body>
</html>
);
}
// app/page.js
export default function Page() {
return <h1>Hello, World</h1>;
}
- /app/member/login/page.tsx : /member/login
- μ°Έκ³ : https://nextjs.org/docs/app/building-your-application/routing/layouts-and-templates
- User λ°μ΄ν° λͺ¨λΈ μμ±
- /server/ λ΄μ routes, models, controller λ₯Ό μμ±νλ€.
- routes/userRoutes.js μ λ°±μλμμ μμ²λ°μ routes λ₯Ό μ μνλ€.
const router = express.Router();
router.route("/")
.post(registerUser)
.get(protect, admin)
router
.post("/login", authUser)
router
.route('/profile')
.get(protect, getUserProfile)
.put(protect, updateUserProfile)
router
.route('/:id')
.delete(protect, admin, deleteUser)
.get(protect, admin, getUserById)
.put(protect, admin, updateUser)
- models/userModel.js μμ μ μ λ°μ΄ν° schema λ₯Ό ꡬμ±ν¨.
const userSchema = mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
isAdmin: {
type: String,
required: true,
default: false,
},
},
{
timestamps: true,
}
);
const User = mongoose.model("User", userSchema);
export default User;
// User model μ User λ‘ μ μν΄μ λ΄λ³΄λΈλ€
- controller/userController.js : registerUser νΈλ€λ¬λ₯Ό ν΅ν΄ userModel μμ κ°μ Έμ¨ User λͺ¨λΈμ λ§μΆμ΄ μ λ ₯λ°μ μ μ λ°μ΄ν°λ₯Ό μμ±νλ€.
const registerUser = asyncHandler(async (req, res) => {
const {name, email, isAdmin, password} = req.body
const userExists = await User.findOne({email})
if (userExists) {
res.status(400)
throw new Error('User already exists')
}
const user = await User.create({
name,
email,
isAdmin,
password,
})
if (user) {
res.status(201).json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: generateToken(user._id),
})
} else {
res.status(400)
throw new Error('Invalid user data')
}
});
- μλ²μμ² λ°©μμ async λ‘ λ³κ²½
- μλ²μλ΅μνμ λ°λΌ 컀μ€ν λ©μμ§ μ μ©
const callDataHandler = async () => {
try {
const {data, status} = await axios.get("http://localhost:5500/getdata");
if (status === 200) {
console.log(data);
setMsg(data);
setCallStatus("suceess");
}
} catch (error: any) {
console.log(error);
setMsg("μλ² ν΅μ μ μ€ν¨νμμ΅λλ€.");
setCallStatus("failed");
}
;
};
<div>
{msg && (callStatus === "suceess") ? <CallStackSuccess/>
: (callStatus === "failed") ? <CallStackFailed/>
: null
}
</div>
- λ°±μλ νΈμΆ ν μ€νΈ
- μ΄λ²€νΈνΈλ€λ¬ (callDataHandler) λ²νΌμ ν΅ν΄μ μλ²μμ²μ ν μ€νΈ
// server.js μΆκ°
server.get("/getdata", (req, res) => {
res.send(`PORT: ${port} μλ²μμ μ μμ μΌλ‘ μλ΅ν¨`);
});
//page.tsx
// μλ² νΈμΆ μ΄λ²€νΈ νΈλ€λ¬ μΆκ°
const callDataHandler = () => {
axios.get("http://localhost:5500/getdata").then((res) => {
console.log(res.data);
setMsg(res.data);
}).catch((error) => {
console.log(error.message);
setMsg(error.message);
});
};
<button onClick={callDataHandler}>λ°μ΄ν° μμ²</button>
- νλ‘ νΈ, λ°±μλλ₯Ό κ°μ΄ κ°μ΄ ꡬμ±νκΈ°λ‘ ν¨.
- src/server/ μμ μλ² κ΅¬λμ μν νμΌ μμ± (server.js)
- νλ‘ νΈ port 3000, λ°±μλ port 5500
//server.js
import express from "express";
import next from "next";
const dev = process.env.NODE_ENV !== "development";
const port = process.env.PORT || 5500;
const app = next({dev, port});
const handle = app.getRequestHandler();
const isDate = new Date();
app.prepare().then(() => {
const server = express();
server.get("/", (req, res) => {
res.send(
`Running SERVER... ${isDate}`
);
});
server.all("*", (req, res) => {
return handle(req, res);
});
server.listen(port, () => {
console.log(
`Server running is ${process.env.NODE_ENV} mode on port ${port}`
);
});
});
- pscksge.json μμ
"scripts": {
"server": "nodemon src/server/server",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
μ€ν
npm run server
npm run dev
- λΈλΌμ°μ μμ localhost:3000, localhost:5500 μ μ΄μ΄μ νμΈ
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
point_app
ββ public
β ββ next.svg
β ββ vercel.svg
ββ src
β ββ app
β β ββ favicon.ico
β β ββ globals.css
β β ββ layout.tsx
β β ββ page.module.css
β β ββ page.tsx
β ββ server
β ββ server.js