일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- SPC
- 공정능력
- MSSQL
- hadoop
- SQL
- Kotlin
- Eclipse
- R
- window
- es6
- react
- plugin
- 보조정렬
- SSL
- mapreduce
- tomcat
- mybatis
- Sqoop
- vaadin
- Java
- Spring
- NPM
- JavaScript
- IntelliJ
- Android
- Express
- table
- xPlatform
- GIT
- Python
- Today
- Total
DBILITY
next.js 13 기초 정리 본문
ssr framework다. java에 vadin framework이 있는데 차라리 그게 나을지도..ㅎㅎ
반백살에 공부해두나 쓸일이 없다.꼰대라 그런가 성격이 나빠서 그런가 갑질을 거부해서 그런가^^
다들 typescript를 쓰는데, 나는 아직 익숙하지 않다.
나는 intellj idea를 사용한다.
new project > Generators > React > project type Next.js > create-next-app 선택 후 생성한다.(현재시점 13.4.2)
Next.js 13.4. The App Router is now stable.
nextjs(13) 에서 route는 directory구조로 처리한다.
/list라는 web page 요청 경로는 디렉토리 /list내의 page.js가 처리한다.
동일 경로상에 layout.js가 있으면 layout으로 page를 감싼다. 상위경로에도 있으면 중첩된다.
각 page마다 별도의 css를 줄 수 있는데 page.module.css에 적용하고, 페이지에서 import해서 사용하면 된다.
import styles from './page.module.css'
App Router를 사용할때 page는 default extention이 ts,tsx,js,jsx다.
parameter를 받는 web page는 /detail/[paramter] 형태로 디렉토리를 생성하고 그 안에 page.js를 생성한다(dynamic segment).
parameter는 page function의 props object에 params object로 존재한다.
중간에 /detail/a/b/c 처럼 전체경로를 parameter로 받을때는 /detail/[...parameter]/page.js 구조로 작성하고,
위와 같은 경우 parameter는 params object내에 array로 존재한다. (props.params = { parameter:['a','b','c'] })
csr(client side rendering)용 콤포넌트나 페이지에서 useRouter(next/navigation)를 사용하여 react의 useNavigate처럼 쓸 수 있다.next/navigation에 redirect도 있다. usePathname, useParams, useSearchParams hook도 사용 가능하다.
UI 이벤트 등을 처리, useState등을 사용하려면 코드 최상단에 'use client'를 명시해야 한다.(Client Component)
'use client'
import {useRouter} from "next/navigation";
export default function LinkButton() {
const router = useRouter();
return(
<>
<button onClick={()=>router.push('/write')}>write</button>
</>
)
}
image삽입에는 img태그와 Image( import Image from "next/image" ) 콤포넌트를 사용한다.
Image콤포넌트의 장점은 lazy loading, image optimization, placeho1der 등이다.
Link태그엔 prefech기능이 true로 마우스를 올려보니 개발자도구 Network에서 말 그대로 fetch가 됨.
data fetching은 공식 사이트를 참고하자. option에 cache,revalidation 등도 있다.
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching
web request,response를 처리하는 Route Handler는 App Router를 사용하려면 app\api 내에 route경로 하위에 route.js로 작성한다.
export async function GET(request) {
const {searchParams} = new URL(request.url);
console.log(searchParams);
return NextResponse.json('GET');
}
export async function POST(request) {
//html form submit
const formData = await request.formData();
const res = Object.fromEntries(formData);
console.log(res);
console.log(res.title, res.content);
return NextResponse.json({res});
}
form post submit 후 NextResponse.redirect로 이동하려면 직접해본 결과 다음과 같이 ( 302 = status code )
return NextResponse.redirect(new URL('/list', request.url),302);
또는
return Response.redirect("http://localhost:3000/list");
또는
return Response.redirect(new URL('http://localhost:3000/list'));
route경로가 request url이 되고 route.js내에 GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS를 function으로 작성한다.
/*
* /api/test?title=1&content=2
*/
export async function GET(request) {
const {searchParams} = new URL(request.url);
console.log(searchParams.get("title"),searchParams.get("content"));
searchParams.forEach((value,key)=>{
console.log(key, value);
});
return NextResponse.json({title:searchParams.get("title"),content:searchParams.get("content")});
}
13에서도 Pages Router(API Route)를 지원하는데 최상위에 pages\api 디렉토리 내에 handler js를 작성한다.
이게 더 쉽게 느껴지는 건 왜 일까?
dynamic segment도 지원한다.
/api/test라는 request url이 필요할 경우 pages\api\test.js를 작성한다.
export default function handler(request, response) {
if (request.method === 'GET') {
const {title, content} = request.query;
return response.status(200).json({title: title, content: content})
} else if (request.method === 'POST') {
const {title, content} = request.body;
//return response.status(200).json({result:'OK'});
return response.status(200).redirect('/list');
} else {
return response.status(500).json({error:'Not supported Request Method'})
}
}
App Router를 사용할 경우 getServerSideProps는 지원되지 않는다.Page Router에서는 된다.
build시 dynamic build여야할 page가 static build가 되는 경우 그 페이지에 다음과 같이 추가하면 dynamic build가 된다
export const dynamic = 'force-dynamic';
build package.json의 scripts부분을 보고 하면 된다.
npm run build
build를 하고 나면 .next라는 directory가 생성되고, 실제 배포는 전체 project directory를 배포하고자 하는 서버에 복사 후
다음과 같이 실행하면 된다.
node_modules는 global install을 해 두면 별도로 복사할 필요가 없을 것 같기도 하고(테스트 안함),src 등 실제 실행에 필요하지 않은 것은 삭제해도 된다.
npm run start
실행 port를 변경하고자 할 경우 package.json의 scripts부분의 수정할 수 있다.다른 방법도 있다. 기억 안남.
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start -p 80",
"lint": "next lint"
}
서비스 모니터링 툴로 PM2가 있다, nodemon인가도 있었는데 모르겠다.(현재까지 찾아 본 것)
file upload는 주로 formidable,multer 등을 사용한다고 한다. 기존 API를 사용하는 경우에는 next api에서 다시 전송하더라.
FormData에 append, fetch시 body에 할당했다.
page.jsx
'use client'
import {useState} from "react";
export default function Write() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [file, setFile] = useState("");
const [fileName, setFileName] = useState("");
const [fileType, setFileType] = useState("");
const regex =/\S+(\.)(jpg|png)/;
const onSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
formData.append("fileName", fileName);
formData.append("fileType", fileType);
formData.append("file", file||"");
for (const pair of formData.entries()) {
console.log(pair)
}
const res = await fetch(e.target.action, {
method: e.target.method,
body: formData
});
const result = await res.json();
console.log(result);
};
const onChange = (e) => {
if (e.target.name === "file") {
let file = null;
if (e.target.files.length > 0) {
file = e.target.files[0];
if( !regex.test(file.name)) {
window.alert("JPG or PNG");
e.target.value = "";
return;
}
if (Number(file.size / 1024 / 1024) > 30) {
window.alert("용량초과");
e.target.value = "";
return;
}
setFileName(file.name);
setFileType(file.type);
setFile(file);
} else {
setFile(null);
}
} else if (e.target.name === "title") {
setTitle(e.target.value);
} else if (e.target.name === "content") {
setContent(e.target.value);
}
};
return (
<div>
<h4>Write</h4>
<form action="/api/fileupload" method={'POST'} onSubmit={onSubmit} encType={"multipart/form-data"}>
<input type="text" name={'title'} required={true} minLength={1} maxLength={10} onChange={onChange}
value={title}/><br/>
<textarea name="content" id="content" cols="30" rows="10" required={true} minLength={1}
maxLength={200} onChange={onChange} value={content}></textarea><br/>
<input type="file" name={"file"} onChange={onChange}/><br/>
<button type={'submit'}>Submit</button>
</form>
</div>
)
}
route.js
import {NextResponse} from "next/server";
import fs from "fs";
import path from "path"
export async function POST(request) {
const savePath = path.join("public/upload/");
let message = "OK";
const formData = await request.formData();
for (const pair of formData.entries()) {
console.log(pair)
}
const body = Object.fromEntries(formData);
try {
if (body.file !== "") {
!fs.existsSync(savePath) && fs.mkdirSync(savePath);
const buffer = Buffer.from(await body.file.arrayBuffer());
await fs.writeFile(path.join(savePath, body.fileName), buffer, (error) => {
if (error) {
console.log(error);
message = error;
}
});
}
} catch (e) {
console.log("catch", e);
message = e.message;
}
return NextResponse.json(message);
}
참고로 form button type submit으로 전송할 경우 method를 PUT,DELETE해도 안된다.
13.4부터 server component에 server action을 줄 수 있다.
별도 route없이 page 내에서 server code를 실행할 수 있다.
다음은 글을 수정하는 page.jsx 예시로 원래는 route.js에서 처리할 걸 page에서 직접 처리 해 본 것이다.
server-only package 서버전용임을 명시하여 build할 때 client component에서 사용하면 오류가 발생한다고 한다.
import Link from "next/link";
import {dbClient} from "@/lib/dbconnect";
import {ObjectId} from "mongodb";
import {redirect} from "next/navigation";
import "server-only";
async function getData(props) {
const client = await dbClient;
const db = client.db("next-study");
const post = await db.collection('board').findOne({
_id: new ObjectId(props.params.id)
});
return post;
}
async function handleSubmit(formData) {
'use server'
const client = await dbClient;
const db = client.db("next-study");
const result = await db.collection('board').updateOne({
_id: new ObjectId(formData.get("_id"))
},
{
$set: {
title: formData.get("title"), content: formData.get("content")
}
});
if(result) {
redirect("/list");
}
}
export default async function Edit(props) {
const post = await getData(props);
return (
<div>
<h4>Write</h4>
{/*<form action="/api/board" method={'POST'}>*/}
<form action={handleSubmit}>
<input type="hidden" name="_id" defaultValue={post._id.toString()}/>
<input type="hidden" name="_method" defaultValue={"PUT"}/>
<input type="text" name={'title'} defaultValue={post.title}/><br/>
<textarea name="content" id="content" cols="30" rows="10" defaultValue={post.content}></textarea><br/>
<button type={'submit'}>Submit</button>
</form>
<Link href={"/list"}>list</Link>
</div>
);
}
왠지 아닐 것 같은데, java Servlet Filter, spring framework의 HandleIntercepter나 ArgumentResolver처럼 동작하게 하는(?) middleware도 있다.
https://nextjs.org/docs/app/building-your-application/routing/middleware
app하위에 작성해 봤다.이러면 global인가?
import {NextResponse} from "next/server";
export async function middleware(request) {
console.log(request);
NextResponse.next();
}
근데 왜 안되지?
Use the file middleware.ts (or .js) in the root of your project to define Middleware. For example, at the same level as pages or app, or inside src if applicable.
src 아래 app와 같은 위치에 저장하니 된다.
조건문 사용은 당연하고, path maching 설정도 가능하다. 다음은 제외 설정
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - auth (auth page)
*/
'/((?!api|_next/static|_next/image|favicon.ico|auth|$).*)',
/*
* '/((?!api|_next/static|_next/image|favicon.ico|auth).{1,})',
*/
],
};
Configured matchers:
1.MUST start with /
2.Can include named parameters:
/about/:path matches /about/a and /about/b but not /about/a/c
3.Can have modifiers on named parameters (starting with :)
:/about/:path* matches /about/a/b/c because * is zero or more.
? is zero or one and + one or more
4.Can use regular expression enclosed in parenthesis:
/about/(.*) is the same as /about/:path*
https://github.com/pillarjs/path-to-regexp#path-to-regexp-1
NextAuth 인증을 사용하면 다음처럼..그냥 spring intercepter를 상상해 보면 되겠다.
import {NextResponse} from "next/server";
import {getToken} from "next-auth/jwt";
export async function middleware(request) {
console.log('middleware -----> ', request.nextUrl.pathname);
/*if (request.nextUrl.pathname.startsWith('/a')) {
return NextResponse.next();
} else if (request.nextUrl.pathname.startsWith('/b')) {
const session = await getToken({req: request});
console.log('middleware session', session);
if (session === null) {
return NextResponse.redirect(new URL('/auth', request.url));
}
}*/
const session = await getToken({req: request});
console.log('middleware session', session);
if (session === null) {
return NextResponse.redirect(new URL('/auth', request.url));
} else {
return NextResponse.next();
}
}
export const config = {
matcher: [
/*'/((?!api|_next/static|_next/image|favicon.ico|auth|$).*)',*/
'/((?!api/auth|_next/static|_next/image|favicon.ico|auth|register).{1,})',
'/api/auth/holidoy/:path*'
]
}
휴.....정규표현식은 왜 이렇게 기억이 되지 않는 걸까?
시험할 수 있는 사이트가 있다.
middleware에서 cookie도 다룰 수 있다.
if(request.cookies.has('weight')) { // exist
request.cookies.get('weight'); // get
request.cookies.delete('weight'); // remove
}
....
// create
const response = NextResponse.next();
response.cookies.set({
name: 'weight',
value: 60,
maxAge: 60 * 60,
httpOnly: true
});
return response;
express custom server를 사용해야 할 경우를 위해 다음을 참고했다.현재 API Router에는 별도로 나와 있지 않다.
이렇게 겁을 주기도 하지만
Before deciding to use a custom server, please keep in mind that it should only be used when the integrated router of Next.js can't meet your app requirements. A custom server will remove important performance optimizations, like serverless functions and Automatic Static Optimization.
https://nextjs.org/docs/pages/building-your-application/configuring/custom-server
const dotenv = require("dotenv");
const express = require("express");
const next = require("next");
const {request, response} = require("express");
//const cors = require("cors");
dotenv.config();
const dev = process.env.NODE_ENV !== "production";
const hostname = 'localhost';
const port = 3000;
const app = next({dev, hostname, port});
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
//server.use(cors());
server.post("/chat", (request, response) => {
return app.render(request, response, "/");
});
server.get("*", (request, response) => {
console.log("get");
return handle(request, response);
});
server.post("*", (request, response) => {
console.log("post");
return handle(request, response);
});
server.listen(port, (error) => {
console.log("listening to "+port);
});
});
sample 보고 해보니 Not Found가 발생했다. 하루를 짜증내며 보냈다. 생각이란 걸 해야 했는데..POST를 지정하지 않음...ALL도 있다는..
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"express": "node server"
},
개발환경에서 fast refresh ( hot reload )에 문제가 발생했다(production에선 관계없음). chrome console을 보니 다음과 같다.
WebSocket connection to 'ws://localhost:3000/_next/webpack-hmr' failed: Connection closed before receiving a handshake response
eval @ use-websocket.js:43
next에서 WebSocket 지원하지 않아 별도로 쓰건데 왜? webpack-hmr? hmr = hot module reload
hmr에 WebSocket을 사용하고 있나 보다. 12부터 SSE에서 WebSocket을 사용하게 되었다고 한다.
framework이라는데 지원하지 않으려나 보다 다른 서비스를 이용하라고 나와 있음.
아무튼 아래는 무시하고 해 본 것이다.
https://nextjs.org/docs/pages/building-your-application/upgrading/version-12
ws랑 stomp는 spring에서 써봤는데 socket.io는 처음이다.
다음과 같이 채팅기능을 작성해 본다.
nextjs는 Websocket을 지원을 안하나보다. Page Router에 custom server를 참고했다.
server.js
const dotenv = require("dotenv");
const express = require("express");
const next = require("next");
const {request, response} = require("express");
/*const cors = require("cors");*/
const http = require("http");
const {Server} = require("socket.io");
dotenv.config();
const dev = process.env.NODE_ENV !== "production";
const hostname = 'localhost';
const port = 3000;
const app = next({dev, hostname, port});
const handle = app.getRequestHandler();
console.log(process.env.NODE_ENV, dev);
app.prepare().then(() => {
const expressServer = express();
/*expressServer.use(cors());*/
expressServer.get("*", (request, response) => {
return handle(request, response);
});
expressServer.post("*", (request, response) => {
return handle(request, response);
});
/*expressServer.listen(port, (error) => {
console.log("listening to "+port);
});*/
const httpServer = http.createServer(expressServer);
const io = new Server(httpServer);
io.on("connection", (socket) => {
console.log(socket.id, 'connected');
socket.on("disconnect", () => {
console.log("disconnected");
});
socket.on("chat", (data) => {
console.log(`${socket.id} : ${data.message}`);
socket.broadcast.emit("chat", {message: data.message});
});
socket.emit("chat", {message:`${socket.id} connected`});
});
httpServer.listen(port, (error) => {
console.log("listening to " + port);
})
});
UI는 다음을 참고하였다.
https://socket.io/how-to/use-with-react
socket.js ( io가 기본 autoConnect고, re-rendering되면 다시 접속이 되어 별도로 )
import io from "socket.io-client";
export const socket = io({autoConnect: false});
src/app/chat/page.js
'use client';
import {socket} from "@/components/socket";
import {useEffect, useState} from "react";
export default function Page(props) {
const [isConnected, setIsConnected] = useState(socket.connected);
const [receiveMessages, setReceiveMessages] = useState([]);
const [message, setMessage] = useState("");
const inputRef = useRef(null);
const onKeyDownHandler = (e) => {
if (e.key == 'Enter' && message !== "") {
socket.emit("chat", {message: message});
setReceiveMessages(prevState => [...prevState, message]);
setMessage("");
inputRef.current.focus();
}
};
const clickHandler = (e) => {
e.preventDefault();
if (message !== "") {
socket.emit("chat", {message: message});
setReceiveMessages(prevState => [...prevState, message]);
setMessage("");
inputRef.current.focus();
}
};
useEffect(() => {
socket.connect();
socket.on("connect", () => {
console.log(`connected server : ${socket.id}`);
setIsConnected(true);
});
socket.on("disconnect", () => {
console.log("disconnected server");
setIsConnected(false);
});
socket.on("chat", (data) => {
console.log(`receive : ${data.message}`)
setReceiveMessages(prevState => [...prevState, data.message]);
});
return () => {
socket.disconnect();
socket.off("connect");
socket.off("disconnect");
socket.off("chat");
}
}, []);
//socket.emit("first Request", { data: "first Reuqest" });
return (
<div style={{marginTop: '100px'}}>
<ConnectionState isConnected={isConnected}></ConnectionState>
<input type="text" name="message" id="message" ref={inputRef} value={message}
onKeyDown={onKeyDownHandler}
onChange={e => setMessage(e.target.value)}/>
<button onClick={clickHandler}>전송</button>
<Messages receiveMessages={receiveMessages}></Messages>
</div>
)
}
function ConnectionState({isConnected}) {
return <p>State: {'' + isConnected}</p>;
}
function Messages({receiveMessages}) {
return (
<ul>
{
receiveMessages.map((event, index) =>
<li key={index}>{event}</li>
)
}
</ul>
);
}
별도 socket server를 만들고 proxy연결 처리도 해 봤다.
socket.js
import io from "socket.io-client";
export const socket = io('ws://localhost:3001',{autoConnect: false,withCredentials:true});
server.js
const http = require("http");
const hostname = 'localhost';
const port = 3001;
const server = http.createServer();
const {Server} = require("socket.io");
//socket cors처리 필요
const io = new Server(server, {
cors: {
origin: 'http://localhost:3000',
allowedHeaders: ["*"],
credentials: true
}
});
io.on('connection', socket => {
console.log(socket.id, 'connected');
socket.on("disconnect", () => {
console.log("disconnected");
});
socket.on("chat", (data) => {
console.log(`${socket.id} : ${data.message}`);
socket.broadcast.emit("chat", {message: data.message});
});
socket.emit("chat", {message: `${socket.id} connected`});
});
server.listen(port, () => {
console.log(`listening on *:${port}`)
});
대충 된다.
동시에 실행하고 싶으면 npm-run-all을 설치하고 다음과 같이
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"server": "nodemon server",
"all": "npm-run-all --parallel dev server"
},
nodemon으로 server를 실행하면 server.js가 변경될때 hot reload가 된다.
당장 알고 싶은 기능은 이정도..
그런데 nextjs app에 로그인했는지는 ...세션은 어쩌지?
뭐라 설명할 수 없지만 websocket접속 할 때 header를 확인해서 session-token이 있는지 봤다..이게 맞는가?아무튼 되기는 한다. websocket client 접속 시나 server cors에 credential설정이 되어 있어서 되나 보다.
io.on('connection', socket => {
console.log(socket.id, 'connected');
try {
socket.on("disconnect", () => {
console.log("disconnected");
});
const cookie = parse(socket.request.headers.cookie);
if (!cookie["next-auth.session-token"]) {
socket.disconnect(true);
} else {
socket.on("chat", (data) => {
console.log(`${socket.id} : ${data.message}`);
socket.broadcast.emit("chat", {message: data.message});
});
socket.emit("chat", {message: `${socket.id} connected`});
}
} catch (error) {
console.log(error);
}
});
'front-end & ui > nextjs' 카테고리의 다른 글
next-auth 기초 정리 (0) | 2023.04.24 |
---|