DBILITY

독거 가능성 100% 노후에 라면값이라도 하게 센스를 발휘합시다!😅
Please click on the ad so that I can pay for ramen in my old age!
点击一下广告,让老后吃个泡面钱吧!
老後にラーメン代だけでもするように広告を一回クリックしてください。

next.js 13 기초 정리 본문

front-end & ui/nextjs

next.js 13 기초 정리

DBILITY 2023. 4. 18. 09:50
반응형

ssr framework다. java에 vadin framework이 있는데 차라리 그게 나을지도..ㅎㅎ

반백살에 공부해두나 쓸일이 없다.꼰대라 그런가 성격이 나빠서 그런가 갑질을 거부해서 그런가^^

다들 typescript를 쓰는데, 나는 아직 익숙하지 않다.

https://nextjs.org/docs

 

Docs | Next.js

Using App Router Features available in /app

nextjs.org

나는 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가 처리한다.

web page route

동일 경로상에 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).

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

 

Data Fetching: Fetching | Next.js

React and Next.js 13 introduced a new way to fetch and manage data in your application. The new data fetching system works in the app directory and is built on top of the fetch() Web API. fetch() is a Web API used to fetch remote resources that returns a p

nextjs.org

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인가도 있었는데 모르겠다.(현재까지 찾아 본 것)

https://pm2.keymetrics.io/

 

PM2 - Home

Advanced process manager for production Node.js applications. Load balancer, logs facility, startup script, micro service management, at a glance.

pm2.keymetrics.io

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

 

Routing: Middleware | Next.js

Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly. Middleware runs before cached co

nextjs.org

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

 

GitHub - pillarjs/path-to-regexp: Turn a path string such as `/user/:name` into a regular expression

Turn a path string such as `/user/:name` into a regular expression - GitHub - pillarjs/path-to-regexp: Turn a path string such as `/user/:name` into a regular expression

github.com

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*'
  ]
}

휴.....정규표현식은 왜 이렇게 기억이 되지 않는 걸까?

시험할 수 있는 사이트가 있다.

https://regexr.com/

 

RegExr: Learn, Build, & Test RegEx

RegExr is an online tool to learn, build, & test Regular Expressions (RegEx / RegExp).

regexr.com

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

 

Configuring: Custom Server | Next.js

By default, Next.js includes its own server with next start. If you have an existing backend, you can still use it with Next.js (this is not a custom server). A custom Next.js server allows you to start a server 100% programmatically in order to use custom

nextjs.org

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

 

Upgrading: Version 12 | Next.js

Using Pages Router Features available in /pages

nextjs.org

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

 

How to use with React | Socket.IO

This guide shows how to use Socket.IO within a React application.

socket.io

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
Comments