정리가 필요한 게시글
react의 프레임워크, react앱을 쉽게 구축할 수 있는 수많은 기능이 있음
reactjs의 풀스택 프레임워크
리엑트는 사용자 인터페이스를 구축하는 라이브러리 대규모 프로젝트를 구성하기위해 다양한 라이브러리를 함께 사용해야됨
react에서 추가된 다양한 기능들. 및 문제해결
풀스택 프레임워크의 이유가 나옴
서버측 렌더링 ssr지원
서버측에서 페이지가 렌더링됨.
리엑트를 서버가 아닌 브라우저에서 화면이 렌더링됨 - > seo와 사용자 경험에서 안좋은 영향을 끼침
페이지, 컴포넌트를 서버에서 미리 렌더링해줌, 화면을 로딩할때 미리 렌더링된 코드를 갖고옴
라우팅
page/page명/page명
react에서 라우팅은 코드로 구현이 되지만
next에서는 pages폴더에서 파일로 구현이 가능함
풀스택프레임워크
백엔드 코드를 추가하는 작업도 쉬워짐
node를 사용해 백엔드 api를 쉽게 작성이 가능하다,
db,인증, 파일을 다루는 백엔드 코드를 쉽게 구현이 가능하다.
pages폴더
파일 기반의 라우팅을 설정할 것이다
다양한 페이지를 정의할 수 있음.
핵심
모든 페이지가 사전 렌더링 됨, 개발자 도구에서 실제 html을 볼 수 있음(pre-rendering)
.jsx가 아닌 js여도 jsx를 사용 가능
개요
크게 3가지 컨텐스
1. basic, 이론
- 파일기반 라우팅
- ssr시 데이터 패칭
- react를 기능을 사용
- api 라우트 (풀스택)
react 복습
자바스크립트 라이브러리 클라이언트측 라이브러리
컴포넌트기반 라이브러리
spa 구축
ㅇㄴ
next.js 라우팅
파일기반의 라우팅(정적 동적 라우팅)
[].js 동적라우팅
동적페이지보다 정적페이지가 더 우선시됨
import { useRouter } from "next/router"
const router = useRouter()
console.log(router.query.id)
router.query.동적파일명
query프로퍼티를 사용해 현재 동적 경로의 path를 가져올 수 있다.
ex) [id].js
/5
output == {id:5}
동적라우팅 확장
동적 라우팅의 중첩
page/client/[id]/[project.js].js 형태로 중첩이 가능하다
사용 예시는 각각다른 client마다 특정 project를 확인할때 사용된다.
동적 라우팅의 최하단에서는 상위의 동적path도 갖고올 수 있다.
page/client/1/2
{id:1, project:2}
catch-all
blog/[...slug].js
blog/1/2/3 url로 접근시
console.log(router.query) -> slug[1,2,3] 배열 형태로 출력됨
ex) 2023/12, 23년 12월의 모든 포스트를 불러올때 / 2023/12/1, 23년 12월에 작성된 특정 포스트를 불러올때 위와 같은 형식으로 작성이 가능하다.
path에 올 수 있는 다중값 뿐만 아니라 path의 다중값 또한 제어가 가능하다
Link 컴포넌트를 사용해 페이지 이동
import Link from "next/link"
두가지 방법으로 라우팅이 가능
1. Link 컴포넌트를 사용
import Link from "next/link";
404페이지
pages/404.js 생성
파일기반 라우터 vs 코드기반 라우터
기존의 event projectdptjsms
페이지 사전 렌더링
데이터 패칭
next는 pre-rendering을 지원(사전로딩)
사전에 모든 html을 랜더링하고 페이지 방분자에게 전송하는 구조
최초 로딩시에 사전 랜더링이 이루어짐 / 페이지를 처음 방문할때
사전 랜더링 되는건 첫번째 페이지만 그 후 웹사이트내 다른 페이지로 이동할땐 csr형식으로 이루어짐(spa)
ex) /user 에 접근하게 되면 react의 경우 접근 -> 데이터 fetching이 이루어지지만
next의 경우 pre-fetching- > /user에 접근 - > 사전에 fetching된 데이터를 불러옴
정적생성과 서버측생성의 차이
정적생성 : 빌드시 모든 페이지가 사전생성됨
ssr : 배포후 요청이 서버까지 오는 바로 그때 생성됨
동적데이터가 아닌 정적데이터를 담은 page는 자동적으로 사전에 렌더링을 진행함
getStaticProps : 페이지를 사전 생성할때 아래 함수도 같이 호출됨
export default function Home(props) {
const { products } = props
return <>1312321</>;
}
export async function getStaticProps(context){
return { props: {
products:[{id:"1", title, "product 1"}]
}}
}
정리
getStaticProps
"빌드 시에 딱 한 번"만 호출되고, 바로 static file로 빌드됩니다. 따라서, 이후 수정이 불가능합니다.
SSG (Static Site Generation) 개념입니다.
앱 빌드 후에 웬만하면 바뀌지 않는 내용 (고정된 내용)이 있는 page가 있는 경우에만 사용하는 것이 좋겠지요?
장점은 호출 시 마다 매번 data fetch를 하지 않으니 getServerSideProps보다 성능면에서 좋습니다.
빌드 후에도 고정되는 내용을 보여주는 페이지에 적합하다.
(ex. 블로그 포스트, 상품 페이지, Docs 등 내용이 고정적이며 SEO가 중요한 페이지)
getServerSideProps
"page가 요청받을때마다" 호출되어 pre-rendering합니다.
SSR (Server Side Rendering) 개념입니다.
pre-render가 꼭 필요한 동적 데이터가 있는 page에 사용하면 됩니다.
매 요청마다 호출되므로 성능은 getStaticProps에 뒤지지만, 내용을 언제든 동적으로 수정이 가능합니다.
next를 사용해 데이터를 패칭과 react와의 차이점
next
react
yarn build
: 배포 이전에 빌드하는 구문, 이때 사전랜더링을 진행함
ISG 증분 정적 생성
increamental static regeneration
페이지를 빌드할때만 한번 생성하는게 아닌
배포후에도 재배포 없이도 계속 업데이트됨
정한 X의 마다 업데이트함
시간이 지나면 서버에서 pre fetch한 가장 최신의 페이지를 제공함
export async function getStaticProps() {
const filepath = path.join(process.cwd(), "data", "data.json");
const jsondata = await fs.readFileSync(filepath);
const data = JSON.parse(jsondata);
return {
props: {
products: data.products,
},
revalidate: 5,
};
}
이 코드에서
return {
props: {
products: data.products,
},
revalidate: 5,
};
revalidate 라는 key 값을 추가함 value는 x초를 의미
x초 마다 페이지를 사전렌더링을 다시 진행함
getStaticProps 다른 return key들
동적매개변수
export async function getStaticProps(context)
context매개변수를 통해 동적 params의 값을 가져올 수 있다.
context 는 key, value형태로 이루어짐
key : 동적 매개변수의 이름([id].js인경우 id됨)
value : 실제 url경로 (/1 인경우 1이 출력됨)
export async function getStaticProps(context) {
const { params } = context;
const productId = params.pid;
const filepath = path.join(process.cwd(), "data", "data.json");
const jsondata = await fs.readFileSync(filepath);
const data = JSON.parse(jsondata);
const product = data.products.find((item) => item.id === productId);
return { props: { loadproduct: product } };
}
해당 코드는 문제가 없어 보이나 에러가 발생함
getStaticPaths 를 사용하라는 에러
페이지를 사전 렌더링시 []로 되어있는 동적 페이지는 사전렌더링을 진행하지 않음. 데이터만 다르고 여러페이지로 구성되어 있음
사전에 렌더링을 하지 않는 [].js 파일에 사전렌더링때 요청하는getStaticProps함수는 적합하지 않음.
getStaticPaths 사용법
동적
return {
paths: [
{ params: { pid: "1" } },
{ params: { pid: "2" } },
{ params: { pid: "3" } },
],
};
}
path 배열을 반환
위와같이 작성하면 동적 페이지가 3번 사전생성됨 value를 3가지 갖는걸 알음
import path from "path";
import fs from "fs";
export default function PHome(props) {
const { loadproduct } = props;
return (
<>
<h1>{loadproduct.id}</h1>
<p>{loadproduct.title}</p>
</>
);
}
//getStaticPaths 에 결과값이 context로 전달이 되면 해당 값에 따라서 사전 렌더링을 진행함
export async function getStaticProps(context) {
const { params } = context;
console.log(params);
const productId = params.pid;
const filepath = path.join(process.cwd(), "data", "data.json");
const jsondata = await fs.readFileSync(filepath);
const data = JSON.parse(jsondata);
const product = data.products.find((item) => item.id === productId);
return { props: { loadproduct: product } };
}
// params 1,2,3 에 대한 경로를 사전 렌더링
// 동적 경로를 지닌 [].js 파일을 사전랜더링하기위함
export async function getStaticPaths() {
return {
paths: [
{ params: { pid: "1" } },
{ params: { pid: "2" } },
{ params: { pid: "3" } },
],
fallback: false,
};
}
fallback 키란
사전에 생성할 페이지가 많을때 사용
//fallback을 true로 하면 일부 페이지만 사전 렌더링이 이루어짐
// 나머지 페이지는 요청이 들어온 순간 사전 렌더링을 진행(사전 랜더링된 페이지가 아니여도 요청을 받아들인다는 뜻)
// 그러나 url에 직접 /1 를 입력하는 경우 에러가 발생
export async function getStaticPaths() {
return {
paths: [{ params: { pid: "1" } }],
fallback: true,
};
}
export default function PHome(props) {
const { loadproduct } = props;
if (!loadproduct) {
return <p>loading .. . . .</p>;
}
return (
<>
<h1>{loadproduct.id}</h1>
<p>{loadproduct.title}</p>
</>
);
}
// 위와같이 loadproduct이 없는경우를 따로 조건문으로 해주어 오류 해결
fallback:"blocking"
blocking으로 해주면
페이지가 서비스 되기 이전에 서버에 완전히 사전생성되도록 된다
요청이 될때 사전렌더링이 진행되기때문에 조금 더 시간이 소요될 수도 있음.
if (!loadproduct) {
return
loading .. . . .
;export async function getStaticPaths() {
return {
paths: [{ params: { pid: "1" } }],
fallback: "blocking",
};
}
export async function getStaticPaths() {
const data = await getData();
const ids = data.products.map((item) => item.id);
const params = ids.map((id) => ({ params: { pid: id } }));
return {
paths: params,
fallback: false,
};
}
//위와같이 필요한 경로를 하드코딩하지 않고 반복문으로 처리도 가능하다
만약 fallback을 true로 하고 사전 랜더링 및 잘못된 해당 path에 id와 일치하지 않아
product가 없는 경우
export async function getStaticProps(context) {
const { params } = context;
console.log(params);
const productId = params.pid;
const data = await getData();
const product = data.products.find((item) => item.id === productId);
if (!product) {
return {
notFound: true,
};
}
return { props: { loadproduct: product } };
}
//getStaticProps 함수 내부에서 product의 유효성 검사를 해준다.
// product가 없는 경우 notFound:true를 return해주어 404페이지로 이동시킨다
이제부터 ssr
매개변수 context는 req, res요청과 응답에 접근할 수 있어 헤더설정이나 쿠키를 추가할 수 있다.
export async function getServerSideProps(context) {
//아래와 같이 현재 path나 req, res에 접근이 가능하다
const { params, req, res } = context;
return { props: { username: "max" } };
}
getStaticProps와의 차이점은
함수가 실행되는 빈도, 시점과 매개변수context로 접근할 수 있는 범위에 차이가 있다.
다시 정리하면
const { params } = context;
로 동적 현재 path를 갖고올때
const { params } = context;
const nowPath = params.[]안에 들어간 문자
getServerSideProps를 사용하면
getStaticProps에서 path를 갖고올땐
getStaticPaths로 path를 전달하고 2번의 로직을 거쳤는데
굳이 getStaticPaths를 사용하지 않고도 path를 가져올 수 있다.
getStaticProps은 빌드시에만 호출되기 때문에 context매개변수로 path로 접근하기 위해서는
getStaticPaths로 경로에 대한 정보를 먼저 반환해주어 사용해야된다.
클라이언트 데이터 패칭은 언제 사용하나?
사전렌더링이 필요없거나 할 수 없는 경우
csr은 useeffect를 사용해 컴포넌트가 데이터를 가져올 수 있게 한다.
useEffect(() => {
fetch("https://nextjs-course-e9243-default-rtdb.firebaseio.com/sales.json")
.then((res) => res.json())
.then((data) => {
const arrSalses = [];
for (const key in data) {
arrSalses.push({
id: key,
username: data[key].username,
volume: data[key].volume,
});
}
console.log(arrSalses);
setSales(arrSalses);
setLoading(false);
});
}, []);
일반적인 csr방법
여기서 한가지 for,.. in 문을 사용해 일반 객체를
[] 배열 안에 담긴 객체로 반환할 수 있다
id: key,
username: data[key].username,
volume: data[key].volume,
여기서 key와 value는 실제 객체로 들어간다
최종 output은 [{id:.., volume:..},{id:.., volume:..}]
이런 형태가 된다.
import { useEffect, useState } from "react";
import useSWR from "swr";
function LastSales(props) {
const [sales, setSales] = useState(props.sales);
const { data, error } = useSWR(
"https://nextjs-course-e9243-default-rtdb.firebaseio.com/sales.json"
);
useEffect(() => {
if (data) {
const arrSalses = [];
for (const key in data) {
arrSalses.push({
id: key,
username: data[key].username,
volume: data[key].volume,
});
}
setSales(arrSalses);
}
}, [data]);
if (error) {
return
error !! ! ! !
;loading . .. .
;return (
<ul>
{sales.map((item) => (
<li key={item.id}>{item.username}</li>
))}
</ul>
);
}
export default LastSales;
export async function getStaticProps() {
const res = await fetch(
"https://nextjs-course-e9243-default-rtdb.firebaseio.com/sales.json"
);
const data = await res.json();
const arrSalses = [];
for (const key in data) {
arrSalses.push({
id: key,
username: data[key].username,
volume: data[key].volume,
});
}
return {
props: {
sales: arrSalses,
},
revalidate: 10,
};
}
위와같이 ssg와 csr을 동시에 사용할 경우
브라우저에서 html이 잘 보이지만 firebase의 db값이 업데이트 되는 경우 view는 업데이트 된 값을 포함해서 노출시키지만 실제 개발자 모드 html에서는 보이지 않는다
시작부터 일부 데이터를 갖고 시작하고 업데이트 되는 부분은 브라우저에서 처리하도록 구현해 두가지 방법을 섞어서도 사용이 가능하다
nextjs 최적화
head태그 추가
import Head from "next/head";
let pagehead = (
<Head>
aaa
)
형식으로 변수에 저장해 {pagehead} 로 재사용해서 사용이 가능
_app.js 파일에서 전역적으로도 생성이 가능하다.
모든 페이지에 있어야할 meta태그 등을 넣어준다
태그가 중복되는경우 마지막에 호출된 태그가 출력됨() _app.js 는 body 내부의 html에 관여하지만 _document.js는 html태그 내부 모든 요소에 관여한다
컴포넌트 재활용
이미지 최적화
import Image from "next/image";
<Image width="100" height="100" src={/${image}
} alt={title} />
width와 height 속성을 필수로 입력해주어야된다 or fill (부모요소 가득)
width와 height는 패치해올때 크기고 실제 크기는 css를 우선시한다.
next/image는 현재 페이지에 도달했을때 구성하는 모든 이미지 요소를 불러오는게
아니라 현재 페이지에서 해당 이미지가 노출될때 불러옴
image컴포넌트의 다른 프로퍼티
공식문서 참고
nextjs API Route
api란
api는 url을 포함하고 있고 브라우저 요청을 받고 데이터를 수집하고 돌려보내는 역할을 한다.
api를 추가하고, 사용해보기
Next.js에서 api를 추가하는 방법은
반드시 page폴더 하위에 api라는 이름으로 폴더를 생성해야된다.
api폴더에 속한 파일들은 특수한 방법으로 처리된다.
react컴포넌트를 내보내는게 아니라 api요청에 대한 서버측에서 함수를 반환한다.
-기본적인 api의 형태
pages/api/feedback
function FEEDBACK_API(req, res) {
res.status(200).json({ message: "succsess" });
}
export default FEEDBACK_API;
반드시 api를 export default 해준다.
하지만 특정 요청에 대한 코드를 실행해야되는데 위와같이 작성하면 모든 요청의 종류에 상관없이 호출된다. 조건문과 method프로퍼티를 사용해 해결해준다
function FEEDBACK_API(req, res) {
if (req.method === "POST") {
};
}
res.status(200).json({ message: "succsess" });
}
export default FEEDBACK_API;
요청의 매소드가 post라면 조건문 안에 구문을 실행해준다
요청한 데이터에 접근해보자
function FEEDBACK_API(req, res) {
if (res.method === "POST") {
const email = req.body.email;
const feedback = req.body.feedback;
const newFeedback = {
id: new Date().toISOString(),
email,
feedback,
};
}
res.status(200).json({ message: "succsess" });
}
export default FEEDBACK_API;
데이터를 요청할땐 body에 데이터가 담겨 들어온다.
export async function getStaticProps(){}
사전 렌더링시 api 호출하기
사전 렌더링시 외부 api말고 같은 서버내에서 정의한 내부 api를 http통신이 불가하다.
그래서 getStaticProps코드 내부에서 api로직을 직접 작성해준다.
api를 구성하는 함수를 export 해준뒤
export function get_path() {
return path.join(process.cwd(), "data", "feedback.json");
}
export function get_fileData(filePath) {
const fileData = fs.readFileSync(filePath);
const data = JSON.parse(fileData);
return data;
}
import { get_fileData, get_path } from "../api/feedback";
export async function getStaticProps() {
const filepath = get_path();
const data = get_fileData(filepath);
return {
props: {
feedbackItems: data,
},
};
}
위와같이 api로직을 작성해주면 된다.
동적 페이지 api구성하기
동적경로
/api/1
/api/2
/api/3
와 같은 동적 경로에 대해 라우터 구성하기
api/[id].js파일 생성
function handler(req, res) {
if (req.method == "GET") {
const feedbackId = req.query.id;
const filepath = get_path();
const data = get_fileData(filepath);
const filteredItem = data.filter((item) => item.id == feedbackId);
res.status(200).json({ filteredItem: filteredItem });
}
}
req.query를 이용해 현재 query값을 가져온다.
page/index
function loadFeedbackHandler(id) {
fetch(/api/${id}
)
.then((response) => response.json())
.then((data) => {
setFeedback(data.filteredItem[0]);
}); // /api/some-feedback-id
}
onclick={()=>loadFeedbackHandler(item.id)} 가 아닌 아래처럼
bind를 사용하는 이유에 대해서
<button onClick={loadFeedbackHandler.bind(null, item.id)}>
onClick 핸들러 함수를 ()=>{onClick={loadFeedbackHandler(item.id)}와 같이 인라인으로 전달하는 방법도 가능합니다. 하지만 이 경우 loadFeedbackHandler 함수는 렌더링 시점에 매번 새로운 함수로 생성되기 때문에 성능상 이슈가 발생할 수 있습니다. 이는 대부분의 경우에는 문제가 되지 않지만, 만약 리스트 아이템이 많거나, 복잡한 계산이 필요한 함수인 경우 성능 저하가 발생할 가능성이 있습니다.
반면에 bind() 메서드를 사용하면 렌더링 시점에 함수가 생성되지 않기 때문에 성능상 이점이 있습니다. bind() 메서드는 렌더링 시점에 함수를 생성하지 않으며, 생성된 함수는 인수가 변경되더라도 다시 생성되지 않습니다. 이를 통해 메모리 사용량과 함수 호출 횟수를 줄일 수 있습니다.
따라서, 리스트 아이템과 같은 반복적으로 렌더링되는 컴포넌트에서는 bind() 메서드를 사용하여 성능을 최적화할 수 있습니다.
page에서 동적 경로를 생성할때
[id], [...id] 키워드를 사용해 생성하였다.
api경로를 지정할때도 위와 같은 키워드를 사용할 수 있다.
기본적으로 api는
root/page/api 폴더 안에 지정하며
root/page/api/user 은 /api/user 경로로 호출하면 해당 api에 접근이 가능하다.
또한 파일 내부 api로직은 모든 요청에 대해 호출하기 때문에 반드시 if(req.method)
를 사용해 요청에 따른 로직은 분리하도록 한다.
파일 내부 api와 일반 함수가 같이 있을 수 있는데
일반함수와 api를 구분짓는건 매개변수로 한다. 인자에 req, res 가 있으면 next.js는 해당 함수를 api로 지정한다.
또한 일반 page를 구성할때와 동일한 규칙을 갖고있다.
api/feedback.js
가 아닌
api/feedback/index.js
api/feedback/[id].js
위처럼 폴더를 구조화 하여 개발하는 방법이 권장된다.
api에 mongodb 연결
yarn add mongodb//mongodb를 사용하기 위해
import { MongoClient } from "mongodb";
const client = await MongoClient.connect(
"mongodb+srv://joseonghong:whtjdghd0127@cluster0.xplbrps.mongodb.net/events?retryWrites=true&w=majority"
);
const db = client.db();
client변수에 mongodb를 연결, .db매서드를 활용해 db에 접근
create(insertOne)
insertOne매서드 활용
const result = db.collection(collection).insertOne(document);
collection은 테이블을 의미함
document는 collection테이블에 추가할 데이터(key, value)
{comment: newComment}
추가하게되면 자동적으로 데이터에 id값이 만들어진다.
데이터를 생성할때 고유 id를 직접 넣을 필요 없이
데이터를 생성한뒤
result변수에 .insertedId 프로퍼티는 데이터를 추가하고 자동으로 만들어진 id에 접근할 수 있다.
newComment.id = result.insertedId;
이런식으로 추가도 가능하다.
read(find)
find()매서드를 활용해서 데이터를 가져올 수 있다.
()내부 아무런 조건이 없으면 모든 데이터를 가져온다
toArray는 가져온 데이터를 배열형태로 반환
sort는 정렬
const allContents = await db
.collection(collection)
.find(filter)
.sort({ _id: -1 })
.toArray();
find내부 조건은 객체 형태로 작성이 가능하다
find({"comments.eventId": eventId,})
comments.eventId가 eventId와 동일한 아이템만 불러오게된다
crud를 진행한뒤 받느식 client.close();를 해주어 연결을 끊어주어야된다
context api를 활용해 상태관리 하기
import { createContext } from "react";
const NotificationContext = createContext({
notification: null, // { title, message, status }
showNotification: function (notificationData) {},
hideNotification: function () {},
});
export default NotificationContext;
위와같이 초기상태를 지정해주고 export 해준다
.Provider value={context} 를 해주어 자식 모두가 context를 사용할 수 있게 해주기 위해