[글또] 3. Next.js와 Netlify로 커미션 홍보 블로그 만들기

jinvicky·2024년 11월 7일
0
post-thumbnail

Intro


취미로 그림을 그리는데 공지사항용 블로그가 만들고 싶어! 마음으로 시작했던 우당탕탕 삽질기입니다. 지금도 계속되고 있는 삽질의 과정을 정리했습니다.
기존에 짧게 올렸던 글들도 조금 연관되어 있습니다.

Start


올해 초, 그러니까 겨울쯤에 취미생활을 프로젝트로 옮겨보고 싶어서
Spring Boot + React로 혼자서 개발에 열을 올리곤 했었다. (지금 생각하면 코드가 참 더럽다….) aws 요금상의 문제 때문에 중단했는데, 취미생활 블로그를 작게 나마 배포해서 사람들에게 보여주고 싶다는 생각을 항상 하고 있었다.

목표 재설정

내가 무언가를 실패했다는 것은 부끄러운 일이 아니지만, 지식과 실력에 비해 너무 높게 거창한 일을 하려 해서 겨울 프로젝트가 흐지부지된게 아닐까 생각했다. 딱 CDN 연동과 사이트 배포 정도만 하면 좋을 것 같은데…..하다가 사내 프로젝트가 코드 프리징에 들어가면서 살짝 시간이 떠서 이 때 결심을 실행에 옮겼다.

사전조사

무료 CDN 서비스를 찾아다니다가 Cloudinary 서비스를 발견했다. Cloudinary는 많은 언어들을 지원하고 따라하기 위한 설명이 친절해서 이걸 선택했다.
배포 사이트의 경우 netlify가 github만 있으면 설정 1번과 버튼 클릭 몇 번으로 배포가 되는 걸 보고 이거다! 싶어서 골랐다.

❗ 나의 경우 netlify function과 netlify를 정확히 차이를 몰랐고, sqlite3과 같은 파일 DB라면 배포해도 CRUD가 될 것이라고 잘못 알아서 나중에 충격을 받았다.
나중에도 나오는 내용이지만 이 글을 읽으시는 분들께 netlify로 CUD를 하려면 netlify functions를 도입해야만 한다.
netlify의 경우 월별 총합 배포시간을 한정되어 있다는 점도 참고하면 좋을 듯 하다.

react, vue3 모두 경험이 있는데 cloudinary에서 지원하는 언어중에 next.js를 보고 새로운 시도를 해보고 싶었다. react를 지원하면서 해외 대기업들이 next.js를 적극 사용하고 있기 때문이다.

시행착오들을 회고해보면 대략 아래와 같았다.

  1. netlify 배포 과정과 cloudinary 서비스 연동하는 방법을 익힌다.
  2. next.js 프로젝트를 만들고 기틀을 잡았다. (router 갈아엎고, 맨땅에 머리박고)
  3. 두근두근 첫 netlify 배포를 한다 (어? 왜 안되지?)
  4. 기능을 하나씩 추가한다. (기본 CRUD도 해보고 pathvariable 방식도 해봄)
  5. 어드민만 아이디/비번으로 특정 페이지에 진입하려면 어떻게 하지? (미들웨어 파봄)
  6. 아 열심히 만들었다. 이제 배포해서 돌아가려나 (netlify에서 배포했을 때 DB가 read-only네????!!!)
  7. netlify function 쓰려다가 그냥…. 안되겠다. 개인 PC라도 구해봐야겠다.

1. Netlify and Cloudinary

Reference

🔖 https://velog.io/@easyxxu/React-netlify-배포하기
빌드 명령어는 npm run build로만 설정했다.

🔖 https://cloudinary.com/guides/front-end-development/integrating-cloudinary-with-next-js

2. Next.js 프로젝트 구조 잡기

next.js 13 버전과 tailwindcss를 사용해서 프로젝트를 만들었다. (명령어는 구글링)
프로젝트를 만들면 디렉토리 구조와 alias와 같은 설정을 제일 먼저 한다.

src 폴더 내부에 둘 것인가? 선택할 수 있고 이에 따라 alias의 baseUrl이 달라진다.

Alias, 디렉토리 절대 경로

jsconfig.json

  ],
    "paths": {
      "@/*": ["./src/*"],
      "@app/*": ["./src/app/*"],
    }
  },
  "include": ["next-env.d.js", "**/*.js", "**/*.jsx", ".next/types/**/*.js"],

여기서 시간을 오래 끌었는데 path와 baseUrl 속성이 틀린 줄 알았는데
정작 include 속성을 고치니 문제가 해결되었다.

Router (Front)

Next.js는 디렉토리가 route 경로가 되는 특징을 가지고 있다.
기존은 page router였다가 13버전부터 app router가 신규 추가되면서 app router를 사용하는 추세다.


pages 폴더를 안 써도 없으면 에러가 나서 추가해야 했다. (초기 프로젝트 설정을 잘못했나)

결과적으로 아래 구조가 된다. (page -> pages)

간략하게 설명을 해보자면,

  • 디렉토리당 /디렉토리명으로 url 경로가 붙는다.
  • Spring의 pathvariable 처럼 /뒤에 uuid 등을 붙이고 싶다면 디렉토리를 [디렉토리명]로 지으면 된다.

    /api/apply/uuid

app폴더 최상단의 파일들은 에러 처리, 로딩, 인덱스 페이지 등이 있는데 react로 따지면 <Suspense> 중첩 구조와 동일하다고 공식에서도 나온다.

복잡한 중첩 태그 대신 그저 파일만 만들면 알아서 404 에러시 보여줄 페이지 등을 처리해준다니 매우 편하다.

Navbar, Footer처럼 전역 페이지에 걸쳐 사용되는 컴포넌트들은 Layout.jsx에 처리한다.

export const metadata = {
  title: "Jinvicky's Commission",
  description: "Official jinvicky's commission blog",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar />
        <Contact />
        {children}
        <Footer />
      </body>
    </html>
  );
}

Navbar에서 라우터 링크와 style 처리를 해본다. next.js에서는 <Link />를 쓴다 (import 주의)
현재 url을 가져오고 싶다면 usePathname을 이용한다.(next/navigation 쓰라고 함)

import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
// ... 생략
const pathname = usePathname();

const linkComponents = links.map((link) => (
    <li
      key={link.id}
      className={pathname === link.href ? "bg-blue-200 p-4" : "p-4"} // stylesByType을 사용하지 않고 직접 적용
    >
      <Link
        href={link.href} // href를 url로 설정
        className={pathname === link.href ? "text-blue-900" : "text-gray-900"} // stylesByType을 사용하지 않고 직접 적용
      >
        {link.name}
      </Link>
    </li>
  ));

Router (Back)

이러면 대략 프론트 페이지가 나오고 페이지별 이동을 할 수 있다.
백엔드와 연동하기 위해서 백엔드 파트는 /api로 시작하게 한다.

폴더별로 route.js를 선언하고 내부에 GET, POST, PUT, DELETE 함수를 선언하면 자동으로 http 메서드별로 매핑이 된다. (최신 문법이다)
NextResponse.json()에 응답 객체를 담는다 (ResponseEntity 느낌). 감싸지 않으면 아래 에러가 발생한다.

⨯ Error: No response is returned from route handler '/Users/namgungjin/Desktop/2024_project/netlify-cms-next/src/app/portfolio/route.js'. Ensure you return a Response or a NextResponse in all branches of your handler.

import { NextResponse } from "next/server";
import { openDb } from "@/app/api/config/db";

import { applySelectOneService } from "@/service/apply";

export async function GET(req, { params }) {
  const { uuid } = params;
  const application = await applySelectOneService(uuid);
  return NextResponse.json({
    status: 200,
    message: "Success",
    data: application,
  });
}
  • 앞서 /apply/uuid 경로에서 uuid 값을 받아오려면 위와 같이 {params}로 받는다.
    (쿼리스트링 파라미터와 다르다)

  • 함수에서 formData, requestBody 등을 받아올 때 async-await이어야만 한다.

    const formData = await req.formData();
    const { userId, password } = await req.json();

NextResponse.json()은 두번째 인자로 응답의 status를 처리하며 기본은 200이다.
에러가 났다면 두번째 인자를 500등으로 처리해야만 한다.
(몰랐다가 에러가 발생했는데 응답이 200으로 오는 걸 겪었다;;)

return NextResponse.json({
      status: err.status || 500,
      message: err.message || "Internal Server Error",
      data: err,
    }, {status: 500});

POST 함수를 2개 선언할 수 없는데 나의 경우 이벤트 소싱? 책에서 본 걸로
command라는 header를 추가해서 분기처리를 했다.

3. 기능 개발

DB

예를 들어서 신청 기능을 구현하려면
1. 폼데이터중 파일 데이터를 CDN에 업로드한다.
2. 업로드를 성공하면 업로드 정보를 신청 테이블에 insert
를 해야 한다.

몇 가지 유의점이 있다.
1. query를 작성할 때 injection 방어를 위해 ?를 사용하고 별도로 파라미터들을 전달해야 한다.

export async function applyFileInsertMapper(db, applyFileRegVO) {
  const {
    applyId, // 신청서 id
    public_id,
    version,
    signature,
    format,
    resource_type,
    asset_folder,
    original_filename,
  } = applyFileRegVO;

  try {
    const result = await db.run(applyFileInsertQuery, [
      uuidv4(),
      applyId,
      public_id,
      `v${version}`,
      signature,
      format,
      resource_type,
      asset_folder,
      original_filename,
    ]);
    return result.lastID;
  } catch (err) {
    throw err;
  }
}

[]안에 별도 전달한다.
2. initDb.js 파일을 만들어서 테이블 DDL을 초기화한다.
3. netlify 배포를 생각하면 최상단에 netlify.toml을 선언하고 아래처럼 작성해야 한다.

[build]
  command = "npm run build"
  publish = ".next"
  functions = "netlify-functions/"

[functions]
  included_files = ["src/db/my-database.db"]
  
[[plugins]]
  package = "@netlify/plugin-nextjs"

Reference
🔖 https://velog.io/@skdbsqls/NextJS-SQLite-데이터베이스-설정-데이터-불러오기

구조는 아래와 같다.

Cloudinary CDN

공식 문서가 잘 되어 있고 코딩도 어렵지 않았다.
다만 필수 폼데이터 항목 말고도 나는 특정 folder에 업로드하고 싶어 등의 정보를 별도 조사했다.

import { NextResponse } from "next/server";
import { headers } from "next/headers";

export async function POST(req) {
  try {
    const formData = await req.formData();
    const file = formData.get("file");

    if (!file) {
      return NextResponse.json({ success: false, message: "no file found" });
    }

    // 헤더로부터 image, video 등의 리소스타입을 받는다.
    const resourceType = headers().get("resourceType");

    const uploadResponse = await fetch(
      `https://api.cloudinary.com/v1_1/${process.env.CLOUDINARY_CLOUD_NAME}/${resourceType}/upload`,
      {
        method: "POST",
        body: formData,
      }
    );

    const uploadedImageData = await uploadResponse.json();
    return NextResponse.json({
      uploadedImageData,
      message: "Success",
      status: 200,
    });
  } catch (error) {
    return NextResponse.json({ message: "Error", status: 500 });
  }
}

Reference

🔖 https://velog.io/@mjieun/Cloudinary-를-사용한-파일-업로드-기능-구현하기
(위젯을 따라하지는 않았지만 소셜 로그인처럼 결국 설정값만 잘 체크하면 된다)

데이터베이스에 저장할 때 full CDN Url을 저장할 수도 있긴 한데, 행여나 파일명이 바뀌면 중간에 어떻게 그 부분만 replace를 하겠나....
cdn 업로드 응답값와 secureUrl을 보고 url 조합에 필요한 정보들로 테이블을 설계해서 저장했다.

실제로 이미지를 띄우는 <Image /> 컴포넌트는 아래처럼 구현했다.

 <Image
	src={`${process.env.NEXT_PUBLIC_CLOUDINARY_BASE_URL}/${item.version}/${item.public_id}.${item.format}`}
	alt={item.original_filename}
	width={300} // 원하는 너비
	height={300} // 원하는 높이
	className="object-cover rounded-lg"
/>

Reference
🔖 https://velog.io/@jinvicky/Next.js-Cloudinary

useSWR()로 API 호출, profile별 baseUrl

api를 다 만들었으면 이제 프론트단에서 불러와야 한다.

원래 react를 하면서 axios를 항상 써왔는데, next.js에서는 더 특별히 최적화된 fetch API를 제공한다고 한다. 그렇다면 fetch를 써야지.

api를 호출할 때 언제까지고 localhost:3000을 쓸 수는 없잖아.
분기처리가 필요하다. (dev, production)
Next.js profile 이라고 구글링하면 공식 문서도 잘 나오고 (아래 포스팅도 참고)

Reference
🔖 https://velog.io/@jinvicky/Next.js-env-profile

원래 항상 useEffect + []랑 useState로 데이터 가져오는 게 보통인데 useSWR()을 써보고 싶었다.

  • SWR로서 api가 에러를 반환해도 기존의 캐시된 데이터를 쓸 수 있다.
  • 불필요한 api 호출을 줄인다.
  • 데이터의 깊은(deep) 비교로 변경이 일어나지 않았다면 리렌더링을 하지 않는다.

useSWR()과 react-query를 비교하는 글을 봤는데 사실 내 자그마한 프로젝트에 그렇게까지…? useSWR()이 조금 과했나?ㅋㅋ 생각도 들었다.

 const {
    data: application,
    error,
    isLoading,
  } = useSWR(
    `${process.env["NEXT_PUBLIC_ROOT_DOMAIN"]}/api/apply/${uuid}`,
    fetchApplyDetailPayload
  );

  if (isLoading) {
    return <Loading />;
  }

  if (application.data === null) {
    // 데이터가 없을 경우 404 호출
    return notFound();
  }

useSWR()을 사용할 때는 로딩중에 대한 처리가 꼭 필요하다. can not read of undefined 단골 에러 보고 싶지 않다면…

tailwindcss 기반의 로딩 컴포넌트 가져다 쓰고 싶으시면 아래 링크 꾸욱 (진행률 처리를 해봤었다)

🔗 https://velog.io/@jinvicky/Next.js-Loading-Handler

또한 해당 데이터가 실제로 디비에 없다면? Next.js가 제공하는 notFound()를 썼다.
Next.js는 아낌없이 주는 나무인 듯? 다 제공을 해준다.

useSWR() 쓰기 전에 useEffect로 개발할 때 []임에도 2번 호출되길래 삽질을 했다. React에서는 18버전부터 strictMode로 개발할 때 dev 환경에서 useEffect가 마운트 시 2번 호출된다고 한다.
난 이 사실을 몰라서 중복 api 호출이라고 생각하고 막으려고 이것저것 뒤졌는데 결국은 무의미한 것이었다...

4. Netlify Trouble Shooting

netlify에 배포하면서 겪었던 이슈들과 해결방법들이다.

netlify.toml 부재 오류

신청 도중 500에러가 났는데 no content다. 이러면 알 길이 없는데..
급한대로 netlify 사이트 logs라도 뒤져본다. 날짜가 방금이다.

테스트해본다고 지웠던 netlify.toml이 문제인가?

맞음. netlify.toml 부활하고 200으로 성공함.

Netlify에 반영이 안된다


아니 빌드를 성공했는데 왜 구버전 사이트지?

난 auto deploy가 싫어서 설정을 막아놨었다.
그래서 trigger deploy를 해도 자동으로 되지 않고 아래 publish deploy 버튼을 눌러야만 실사이트에 반영이 된다.

그리고 .env 파일에 환경변수를 설정했다면 netlify에도 꼭 환경변수 설정 쪽에 가서 변수를 추가해야 한다.

Outro


이번 목표는 CDN 연동과 개인사이트를 실배포하는 것이었습니다.
netlify, cloudinary, next.js, sqlite3라는 기술 스택으로 새로운 도전을 한 것은 좋았지만, DB 관련이 사전 지식이 부족했네요. next.js는 저에게 새로운 도전이었는데 프로젝트의 구조와 app router를 학습하는 좋은 기회가 되었습니다.

🔗 https://jinvicky-commission.netlify.app

Github (private)

🔗 https://github.com/jinvicky/netlify-cms-next

Preview


현재는 방향을 바꿔서 무료 몽고디비와 Render 사이트를 이용해서 오픈카톡 커미션 신청 게시판을 만들어서 신청자분들의 리뷰를 꾸준히 기록하고 있습니다.
기존에 올렸던 커미션 그림들은 워터마크가 추가되면서 CDN과 디비에 저장한 정보들은 변경 및 재설계가 필요해서 진행중에 있습니다. 나중에라도 보여드릴 수 있다면 좋을 것 같아요.

다음 4회차에는 왜 배포를 위해 Render를 선택했고 Docker를 사용한 배포 후기를 작성해 보려고 합니다.
아래에서 제가 만든 리뷰 게시판을 보실 수 있습니다:)

🔗 https://ktalk-review.netlify.app/review

Other Posts

🔗 https://velog.io/@jinvicky/Netlify-Next.js를-배포해봤습니다

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글