NextJS는 대규모의 양산형 Raect 앱을 더 편리하게 구축할 수 있도록 많은 기능을 제공합니다. 풀스택 프레임워크입니다!
페이지 콘텐츠를 전저으로 서버에서 준비하는 것이 서버 사이드 렌더링입니다.
라우팅이란 사용자에게 여러 페이지가 있는 것처럼 착각하게 하는 것입니다.
NextJS는 풀스택 프레임워크입니다.
npx create-next-app
의 명령어가 Node.js에서 필요합니다.
이제 Next.js로 세 페이지로 구성된 간단한 웹사이트를 만들어 보겠습니다!
page폴더에
도메인 뒤에 /까지만 있는 요청인 index.js
도메인 뒤에 /news가 들어가는 news.js
중요한 것은 page 폴더안에 있는 js 파일명이 도메인 주소가 된다는 것입니다!
npm run dev
를 실행해야 시작이 됩니다!
/news 를 입력했을 떄 News Page가 잘 뜨는 것이 확인이 됩니다!
경로를 추가하는 방법이 하나 더 있습니다!
기존 news.js를 page폴더안에 하위 폴더로 news폴더를 만들고 news폴더안에 news.js를 옮겨서 index.js로 이름을 바꿔줍니다! 그리고 news 폴더 안에 또 다른 파일이름인 something-important.js 파일을 만들어주면 중첩 경로가 생깁니다!
이제 /news로 시작하는 페이지가 두 개 생성된 것입니다!
하드코딩된 something-importnat 페이지는 옳지 않습니다. 대신에 동적 페이지라는 것을 만들어야 하는데 /news/ 뒤에 뉴스 항목의 식별자가 어느 것이든 될 수 있습니다! []로 이름을 바꿔주면 Next.js에서는 이것을 동적 페이지로 인식해서 경로에 여러 값을 불러옵니다.
이렇게 되면 /news/ 뒤 아무 것이나 입력하면 The Detail Page가 보이는 것이 확인이 됩니다!
이제 /news/ 뒤 [newsId] 값을 추출해 보겠습니다!
그러기 위해서는 hook을 사용하여야 하는데
import { useRouter } from "next/router";
를 import하고
const router = useRouter();
다음 query를 이용하여 newsId를 불러옵니다. 이떄 newsId는 대괄호로 입력한 [newsId].js 파일 이름과 동일하게 하여야 합니다.
console.log(router.query.newsId)
콘솔 창에 띄어보면
something-else가 보이는 것이 확인이 됩니다. 하지만 앞에 undefine이 보이는데 이것은 리액트 동작방식이 처음 렌더링될 때 한 번 실행하고 그 다음 맞는 URL에 대한 Router를 실행하기 때문에 두 번 출력 되는 것입니다!
const newsId = router.query.newsId
이제 이것을 변수에 담으면 백엔드에서 뉴스 항목을 불러올 수 있습니다!
이제 클릭해서 페이지를 이동해 보겠습니다!
import Link from "next/link";
Link를 사용하여 싱글페이지 애플리케이션에서 새 HTML 가져오지않고 React에서 렌더링합니다!
return (
<Fragment>
<h1>The News Page</h1>
<ul>
<li>
<Link href="/news/nextjs-is-a-great-framework">
Next JS Is A Great Framework
</Link>
</li>
<li>Something Else</li>
</ul>
</Fragment>
);
전체를 Fragment로 감싼 후 [newsId].js로 이동할 Link와 href 주소를 적어주면 끝입니다!
클릭하면
href에 하드코딩된 주소로 이동하는 것을 볼 수 있습니다!
지금까지 Next.js의 기본 동작을 살펴봤고, 이제 이 것들을 더 자세히 활용하기 위해
모임 애플리케이션 사이트를 만들어 보겠습니다!
page폴더에 페이지 세 개가 필요합니다!
모임 목록을 모두 보여주는 페이지
모임 추가 페이지
모임 상세 페이지도 있어야 합니다.
첫 화면 렌더링 페이지를 위해 page폴더에 index.js라는 이름으로 만듭니다.
다음 모임 추가 페이지에 필요한
new-meetup/index.js로 만듭니다! 이 떄, news-meetup 폴더를 만들고 그 하위 폴더 안에 index.js를 만들면 /new-meetup 도메인 주소가 이렇게 뜨게 됩니다!
다음으로 상세 페이지를 만들어야하는데 이 것은 동적 페이지로 만들어야 할 것 입니다. 각가 다른 ID를 갖는 여러 모임이 있고 그 ID 는 URL에 포함될 것이고, 이 IUD를 사용해 데이터를 가져오고 화면에 띄웁웁니다!
[meetupId]라는 폴더를 page안에 만들고
하위 폴더 안에 index.js라는 이름으로 만듭니다!
이것으로 준비가 완료됐습니다.
Components 폴더에 MeetupList.js 컴포넌트를 index.js에 불러와서 첫 화면에 렌더링돼야 합니다!
function MeetupList(props) {
return (
<ul className={classes.list}>
{props.meetups.map((meetup) => (
<MeetupItem
key={meetup.id}
id={meetup.id}
image={meetup.image}
title={meetup.title}
address={meetup.address}
/>
))}
</ul>
MeeupList.js에서는 현재 props로 key,id,image,title,address를 meetups로 받고있어서 이것을 사용하기 위해 index.js에서 DUMMEY로 넘겨주고 index.js에서 MeetupList를 import 해주면 됩니다!
import MeetupList from "../components/meetups/MeetupList";
MeetupList를 import 해주고
const DUMMY_MEETUPS = [
{
id: "m1",
title: "A First Meetup",
image:
"https://upload.wikimedia.org/wikipedia/commons/1/12/View_of_saint_Peter_basilica_from_a_roof.jpg",
address: "Some address 5, 12345 Some City",
description: "this is a first meetup!",
},
{
id: "m2",
title: "A Second Meetup",
image:
"https://upload.wikimedia.org/wikipedia/commons/1/12/View_of_saint_Peter_basilica_from_a_roof.jpg",
address: "Some address 10, 12345 Some City",
description: "this is a Second meetup!",
},
];
더미를 배열 객체로 만들어주고 각각 id와 title image 등을 입력해줍니다!
function HomePage() {
return <MeetupList meetups={DUMMY_MEETUPS} />;
}
다음으로 MeetupList에 meetups props로 DUMMY_MEETUPS를 보내면 끝입니다!
Components폴더 하위 폴더인 meetups 폴더에
NewMmetupForm.js에서 title/image/address/description을 onAddMeetup이라는 Property를 이용해 수집한 모임 데이터를 넘겨줍니다.
이것을 new-meetup/index.js에 불러옵니다!
import NewMeetupForm from "../../components/meetups/NewMeetupForm";
function NewMeetupPage() {
function addMeetupHandler(enteredMeetupData) {
console.log(enteredMeetupData);
}
return <NewMeetupForm onAddMeetup={addMeetupHandler} />;
}
NewMmetupForm.js에 onAddMeetup Property로 addMeetupHandler 함수를 호출하고 입력 양식 데이터를 콘솔 창에서 보여지도록 해보았습니다
http://localhost:3000/new-meetup 이동하면
입력한 값들이 콘솔창에 확인이 되는 것을 볼 수 있습니다!
이제 레이아웃과 헤더 부분을 완성해보도록 하겠습니다!
return (
<div>
<MainNavigation />
<main className={classes.main}>{props.children}</main>
</div>
);
현재 Layout.js는 props.children을 이용하여 Layout으로 감싸는 컴포넌트들을 보여지게 하고있습니다.
실제로 첫 화면인 index.js에 Layout 컴포넌트를 감싸주면 Header가 보이게 됩니다.
function HomePage() {
return
<Layout>
<MeetupList meetups={DUMMY_MEETUPS} />
</Layout>;
}
이런 식으로 감싸주면
보이는 것이 확인이 됩니다.
new-meetup page에도 Layout 컴포넌트를 추가해서 Header가 보이게 할 수 있습니다. 하지만 애플리케이션에 페이지 수가 많으면 번거러올 수 있습니다. 이럴 때 편리하게 사용하는 것이 _app.js를 이용하는 것입니다! 이건 최상위 컴포넌트 같은 겁니다!
index.js에서 Layout을 없애고 _app.js에 Layout을 추가해주면 모든 페이지에 적용이 됩니다!
import Layout from "../components/layout/Layout";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
export default MyApp;
실제 데이터 가져오기 작업과 백엔드로 보내기 전에 세부 페이지를 살펴보겠습니다!
meeupId 페이지를 읽어 들이는 것입니다.
먼저 MeetupItem.js에서 링크를 만들어야합니다!
<div className={classes.actions}>
<button onClick={showDetailsHandler}>Show Details</button>
</div>
button에 Link를 추가해서 페이지 이동이 가능하지만 현재는 프로그래밍 방식으로 사용해보겠습니다!
import { useRouter } from "next/router";
const router = useRouter();
function showDetailsHandler() {
router.push("/" + props.id);
}
useRouter hook을 이용하고 router를 push를 해서 경로를 적어주되 그 경로눈 MeetupList에서
return (
<ul className={classes.list}>
{props.meetups.map((meetup) => (
<MeetupItem
key={meetup.id}
id={meetup.id}
image={meetup.image}
title={meetup.title}
address={meetup.address}
/>
))}
</ul>
id를 MeetupItem에 보내주고 있기 떄문에
router.push("/" + props.id); props.id로 받아서 url에 보여지게 할 수 있습니다!
Show Details를 클릭하면
현재는 [meeupId].index.js에 아무것도 적지 않아서 오류가 뜨는데 URL주소가 id 값인 /m1으로 바뀌는 것을 볼 수 있습니다.
먼저 component를 조금 더 깔끔하게 보이기 위해서 MeetupDetail.js를 만들고
import classes from "./MeetupDetail.module.css";
function MeetupDetail(props) {
return (
<section className={classes.detail}>
<img src={props.image} alt={props.title} />
<h1>{props.title}</h1>
<address>{props.address}</address>
<p>{props.description}</p>
</section>
);
}
export default MeetupDetail;
props를 이용하여 title/address/description을 받아보도록 하겠습니다!
import MeetupDetail from "../../components/meetups/MeetupDetail";
function MeetupDetails() {
return (
<MeetupDetail
image="https://upload.wikimedia.org/wikipedia/commons/1/12/View_of_saint_Peter_basilica_from_a_roof.jpg"
title="First Meetup"
address="Some Street 5, Some Ciry"
description="This is a first meetup"
/>
);
}
export default MeetupDetails;
MeetupDetail에 image/title/addres/description propety를 보내줍니다!
그리고 깔끔하게 하기 위해서 css를 만들건데 css는 components폴더에 MeetupDetail.module.css를 만들겁니다.
이렇게 하는 이유는 page폴더에는 css파일을 만들지않고 깔끔하게 하기 위해 components 폴더에 css를 모아났습니다!
.detail {
text-align: center;
}
.detail img {
width: 100%;
}
이제 index.js에 있는 임시데이터 소스를 실제 백엔드 데이터 소스로 대체해 보겠습니다.
function HomePage() {
const [loadedMeetups, setLoadedMeetups] = useState([]); //상태관리하기 위해 사용
useEffect(() => {
// Http 요청을 보내기 위해 사용
// send a http request and fetch data
setLoadedMeetups(DUMMY_MEETUPS);
}, []);
return <MeetupList meetups={loadedMeetups} />;
}
export default HomePage;
http 요청을 보내기 위해서 useEffect hook을 사용하고 상태관리를 위해 useState hook을 사용하여서 초기값을 빈 배열로 만들어 http요청을 했다 가정하고 DUMMY 데이터를 setLoadedMetups에 에 넣었습니다.
여기서 문제가 발생합니다. useEffect는 React에서 화면이 처음 렌더링 된 후에 useEffect가 렌더링 됩니다. 즉 처음에는 빈 배열이 렌더링이 되는 것입니다. 이 것은 NextJs를 사용할 떄 문제가 되는데 NextJS는 두번째 렌더링되는 것을 기다려주지 않습니다. 따라서 빈 배열로 담아지게 되는 것입니다. 하지만 이것을 해결하는 NextJS 기능이 있습니다. 그 기능을 알아보겠습니다!
page component file 에서 데이터를 가져와서 추가해야 한다면 page component file 안에서 특수 함수를 export 하면 됩니다! 이것은 페이지 컴포넌트 파일에서만 적용이되고 다른 컴포넌트에는 해당이 안됩니다!
export function getStaticProps() {}
반드시 getStaticProps 라고 해야합니다.
NEXTJS는 getStaticProps 이름을 가진 함수를 실행합니다!
이 함수는 실제로 페이지에서 사용할 props를 준비합니다.
export async function getStaticProps() {
...
}
... 안에는 서버에서도 클라이언트 측에서도 실행되지 않습니다.
export async function getStaticProps() {
// fetch data from an API
return {
props:{
meetups: DUMMY_MEETUPS
}
};
}
이렇게하면 getStaticProps 에서 이 DUMMY_MEETUPS를 읽어 들이고 준비한 다음
이 페이지 컴포넌트에서 사용할 props로 설정됩니다. 이제 상태를 관리할 피룡가 없고 useEffect도 필요 없습니다!
function HomePage(props) {
return <MeetupList meetups={props.meetups} />;
}
export async function getStaticProps() {
// fetch data from an API
return {
props: {
meetups: DUMMY_MEETUPS,
},
};
}
export default HomePage;
이제 사전 렌더링도 했고 초기에 이 페이지를 사전 렌더링하기 전에 빌드 프로세스에서 받았습니다. 이 사전 렌더링으로 데이터 가져오기는 굉장한 장점이자 NextJS의 기능 중 하나입니다.
npm run build
빌드 명령어인데 NextJS로 배포하기 전에 실행해야 하는 겁니다.
export async function getStaticProps() {
// fetch data from an API
return {
props: {
meetups: DUMMY_MEETUPS,
},
revalidate: 10
};
}
revalidate에 숫자가 있으면 10m/s 마다 데이터를 새로 불러일으킨다는 것입니다! 규칙적으로 데이터를 새로 업데이트 가능합니다!
export async function getServerSideProps(context) {
const req = context.req;
const res = context.res;
return {
props: {
meetups: DUMMY_MEETUPS,
},
};
}
getServerSideProps는 요청이 들어올 때 마다 페이지를 바로 다시 만드는 방법입니다.
맥변수로 context를 받고 req/res를 받아서 return은 props만 해줍니다.
export async function getStaticProps(context) {
// getch data for a single meetup
const meetupId = context.params.meetupId;
console.log(meetupId);
return {
props: {
meetupData: {
image:
"https://upload.wikimedia.org/wikipedia/commons/1/12/View_of_saint_Peter_basilica_from_a_roof.jpg",
id: meetupId,
title: "First Meetup",
address: "Some Street 5, Some Ciry",
description: "his is a first meetup",
},
},
};
}
getStaticProps에도 context를 이용할 수 있습니다. 현재 동적페이지 id값을 가져오기 위해서 우리는 useRouter를 이용하였지만, 그것은 function에서는 사용이 불가능합니다. 그러기 위해서 context.params.meetupId 를 이용하여 id값을 불러올 수 있습니다!
앞서 getStaticprops만으로는 에러가 뜹니다.
getStaticPaths도 export 해줘야 합니다!
export async function getStaticPaths() {
return {
fallback: false,
paths: [
{
params: {
meetupId: "m1",
},
},
{
params: {
meetupId: "m2",
},
},
],
};
}
fallback을 false로 하면 입력된 params인 meetupId의 m1/m2 외에 다른 것이 들어오면 404페이지를 보여줍니다!
true를 하게되면 알아서 페이지를 생성해줍니다.
API 라우트를 이용해 fetch를 쉽게할 수 있습니다.
API 폴더를 만들고 new=meetup.js를 만들겠습니다.
function handler(req, res) {
if(req.method === 'POST'){
const data = req.body;
const { title, image, address, description} = data;
}
}
export default handler;
npm install mongodb
로 설치를 끝낸 후
import { MongoClient } from "mongodb";
async function handler(req, res) {
if (req.method === "POST") {
const data = req.body;
const client = await MongoClient.connect(
"mongodb+srv://kkary:q52wBa73rIVg9okN@cluster0.dvgiicq.mongodb.net/meetups?retryWrites=true&w=majority"
);
const db = client.db();
const meetupsCollection = db.collection("meetups");
const result = await meetupsCollection.insertOne(data);
console.log(result);
client.close();
res.status(201).json({ message: "Meetup inserted!" });
}
}
const meetupData = {
title: enteredTitle,
image: enteredImage,
address: enteredAddress,
description: enteredDescription,
};
props.onAddMeetup(meetupData);
}
에서 onaddMeetup property로 meetupData를 보냅니다.
따라서 new-meetup페이지로 이동해보겠습니다.
new-meetup/index.js
// our-domain.com/new-meetup
import { useRouter } from "next/router";
import NewMeetupForm from "../../components/meetups/NewMeetupForm";
function NewMeetupPage() {
const router = useRouter();
async function addMeetupHandler(enteredMeetupData) {
const response = await fetch("/api/new-meetup", {
method: "POST",
body: JSON.stringify(enteredMeetupData),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);
router.push("/");
}
return <NewMeetupForm onAddMeetup={addMeetupHandler} />;
}
export default NewMeetupPage;
Router를 이용해 push를 해서 버튼 클릭 시 첫 화면으로 렌더링되게 할 수 있습니다!
const response = await fetch("/api/new-meetup",
이 있는데 이 것은
api폴더에 new-meetup.js로 fetch를 하겠다는 것입니다!
import { MongoClient } from "mongodb";
// /api/new-meetup
// POST /api/new-meetup
async function handler(req, res) {
if (req.method === "POST") {
const data = req.body;
const client = await MongoClient.connect(
"mongodb+srv://[name]:[password].gjbofav.mongodb.net/meetups?retryWrites=true&w=majority"
);
const db = client.db();
const meetupsCollection = db.collection("meetups");
const result = await meetupsCollection.insertOne(data);
console.log(result);
client.close();
res.status(201).json({ message: "Meetup inserted!" });
}
}
export default handler;
mongoDB와 연결시켜주고 성공하면 콘솔 창에 message를 띄울 것입니다!
ADd Meetup을 누를 시 500에러가 떴습니다!
터미널 창 확인하기!
error - MongoServerSelectionError: connection <monitor> to 13.124.16~~~ closed
라는 에러가 떴습니다! 검색을 해보니 MongoDB 사이트에서 Network Access 주소를 현재 주소로 다시 바꿔주면 해결이 됐습니다!
이제 mongoDB에서 데이터를 가져와보겠습니다!
그러기 위해서는 새로운 api파일인 meetups.js를 만들어야했지만 index.js에서 getStaticProps는 서버에서만 보이고 클라언트에게는 보이지 않습니다. 따라서 이 부분에서 MangoClient를 바로 연결해보도록 하겠습니다!
import { MongoClient } from "mongodb";
MongoClient를 import 해주고
xport async function getStaticProps() {
// fetch data from an API
const client = await MongoClient.connect(
"mongodb+srv://kkary:TBh5K9FZhwnwHX1C@cluster0.gjbofav.mongodb.net/meetups?retryWrites=true&w=majority"
);
const db = client.db();
const meetupsCollection = db.collection("meetups");
const meetups = await meetupsCollection.find().toArray();
client.close();
new-meetup.js에서 연결 구문을 가져와
meetupsCollection.find()를 통해 toArray(); 배열로 가져와서 meetups에 넣고,
return {
props: {
meetups: meetups.map((meetup) => ({
title: meetup.title,
address: meetup.address,
image: meetup.image,
id: meetup._id.toString(),
})),
},
revalidate: 1,
};
meetups에 map을 이용해 매개변수 meetup에 title/address/image를 각각 넣고 id는 현재 백엔드에서
_id : 63326569418a600fcb584b87 알아보기 힘든 문자열로 되어있어서 toString()으로 처리해줘야 합니다!
이로서 백엔드에서 데이터를 가져오는 것에 성공했습니다!
[meetupId]/index.js에서 상세 페이지에서 데이터를 가져와보겠습니다!
export async function getStaticPaths() {
const client = await MongoClient.connect(
"mongodb+srv://kkary:TBh5K9FZhwnwHX1C@cluster0.gjbofav.mongodb.net/meetups?retryWrites=true&w=majority"
);
const db = client.db();
const meetupsCollection = db.collection("meetups");
const meetups = await meetupsCollection.find({}, { _id: 1 }).toArray();
client.close();
return {
fallback: false,
paths: meetups.map((meetup) => ({
params: { meetupId: meetup._id.toString() },
})),
};
}
const meetups = await meetupsCollection.find({}, { _id: 1 }).toArray();
첫 번쨰 매개변수에는 객체를 담을 빈 객체과 두 번째 매개변수로는 id 값 하나만을 가져오기 위한 것입니다!
export async function getStaticProps(context) {
// fetch data for a single meetup
const meetupId = context.params.meetupId;
const client = await MongoClient.connect(
"mongodb+srv://kkary:TBh5K9FZhwnwHX1C@cluster0.gjbofav.mongodb.net/meetups?retryWrites=true&w=majority"
);
const db = client.db();
const meetupsCollection = db.collection("meetups");
const selectedMeetup = await meetupsCollection.findOne({
_id: ObjectId(meetupId),
});
client.close();
return {
props: {
meetupData: {
id: selectedMeetup._id.toString(),
title: selectedMeetup.title,
address: selectedMeetup.address,
image: selectedMeetup.image,
description: selectedMeetup.description,
},
},
};
}
getStaticProps에도 MongoDB를 연결시켜주고
const selectedMeetup = await meetupsCollection.findOne({
_id: ObjectId(meetupId),
});
이떄는 아이디 하나만 찾을 것이기 떄문에 findOne을 써준다!
function MeetupDetails(props) {
return (
<MeetupDetail
image={props.meetupData.image}
title={props.meetupData.title}
address={props.meetupData.address}
description={props.meetupData.description}
/>
);
}
그리고 props를 이용해 각각의 속성들을 가져오면 끝!
상세페이지에 입력한 것들이 잘 보이는 것이 확인이 된다!
이제 애플리케이션을 deploy해볼 건데, 해보기 전에 head에 이름을 추가해보겠습니다!
import Head from "next/head";
return (
<Fragment>
<Head>
<title>React Meetups</title>
</Head>
<meta name='description' content='Browsw a huge list of highly active React meetuupos!'/>
<MeetupList meetups={props.meetups} />
</Fragment>
다음 상세페이지인 [meetupId]/index.js 똑같이 해보겠습니다! 하지만 여기서는 head의 title과 description이 하드코딩된 것이 아니라 게시물에 따라서 다르기 때문에 props를 이용해야 합니다!
return (
<Fragment>
<Head>
<title>{props.meetupData.title}</title>
<meta name="description" content={props.meetupData.description} />
</Head>
<MeetupDetail
image={props.meetupData.image}
title={props.meetupData.title}
address={props.meetupData.address}
description={props.meetupData.description}
/>
</Fragment>
);
이제 deployment할 일만 남았습니다!
호스팅 제공자는 Vercel를 이용하겠습니다!
Vercel은 NextJS 애플리케이션을 위한 훌륭한 호스팅 제공 업체입니다.
github로 접속하여 new repository를 만듭니다!
그 전에 https://git-scm.com/download/win 접속하여 git을 설치합니다!
git add .
git commit -m "ready for deployment"
설치 후 터미널 창에 입력 후
repositoty를 만든 후 페이지의 URL주소를 복사 후 origin을 추가하기 위해
git remote add origin
git remote add origin 뒤 복사한 URL 주소를 붙여넣으면 됩니다!
git push --all
이제 여기에 코드가 push되고
암호를 입력하라는 메시지가 뜨면 github에서 Settings에서 token을 이용할 수 있습니다.
npm run build
npm start
npm run build를 이용해야 npm start로 프로덕션 서버를 시작할 수 있습니다.
이것은 원격 기기에서 사용할 수 있는 사용자가 직접 관리하는 서버입니다.
하고 다음으로 mangoDB에서 어디에서나 엑세스가 가능하게 해야합니다.
Error가 났었는데, 원인이 무엇인 지 몰랐다. Repository를 다시 만들고
git remote add origin https://github.com/gkkary3/nextjs-course.git
git branch -M main
git add .
git commit -m "message"
git push -u origin main
순으로 다시 명령을 하고 deploy하니 성공하였다!!
이미지를 클릭하면 새로운 창으로 url주소가 뜨고 https://nextjs-course-5ujs.vercel.app/
로 들어가면 누구나 볼 수 있게 배포가 완성 되었다!!