2차 업데이트_rambling

jinvicky·2024년 9월 12일
0

Intro


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

운영중인 블로그 2차 업데이트를 진행하면서 어떤 변화가 있었는지 적어보았다.

정말 생겼던 이슈들을 일단 다 적었다.
추후에 분류해볼 예정이다.

Updates


  • 신청 개별/대량 상태 변경 PUT API 추가
  • 신청 개별/대량 삭제 DELETE API 추가
  • 신청 개별 추가 POST API 추가
  • 신청 조회 GET API 추가
  • 신규 포트폴리오 페이지 개설
  • cloudinary upload 추가

목표가 변경되면서 업데이트 내용들도 변경되었다.

Developer Note


SQL Injection 방어

처음에 신청 추가 post를 짤 때는 아래와 같이 짰다.

const query = 
        `INSERT INTO apply (
            UUID, 
            CUSTOMER_NAME, 
            GENRE, 
            CATEGORY, 
            STATUS_CODE, 
            REGISTRATION_DATE
        ) VALUES (
            '${uuidv4()}', 
            '${customerName}', 
            '${genre}', 
            '${category}', 
            '${statusCode}', 
            CURRENT_TIMESTAMP
        )`;

하지만 이 방식은 sql injection 위험이 있다. 따라서 ?를 사용한 구조로 바꾸어야 한다.
신청 상태 수정 put에서 아래와 같이 변경하였다.

const query = `
          UPDATE apply
          SET STATUS_CODE = ?
          WHERE UUID = ?
      `;

await db.run(query, [statusCode, uuid]);

return NextResponse.json({
  status: 200,
  message: "Success",
  data: null
});

[]안에 파라미터들을 넣는 구조.

Pathvariable 방식 사용하기

스프링에서 파라미터를 전달할 때 ?파라미터명= 을 쓰는 @RequestParam을 사용하거나 /{값} 을 쓰는 @PathVariable을 사용할 수 있었다.

next.js에서 pathvariable 방식을 어떻게 사용할까?
개별 delete를 예시를 들어 구현해 보았다.

export async function DELETE(req, {params}) {
    const { uuid } = params;
    const db = await openDb();
  
    try {
        const query = `
            DELETE FROM apply
            WHERE UUID = ?
        `;
  
        const result = await db.run(query, [uuid]);
  
        return NextResponse.json({
            status: 200,
            message: "Success",
            data: result
        });
    } catch (err) {
        return NextResponse.json({
                status: err.status || 500,
                message: err.message || "Internal Server Error",
                data: null
        });
    } finally {
        await db.close();
    }
}

코드는 별거 없다. 다만 폴더의 위치가 다르다.

[uuid] 폴더는 nextjs에서 Dynamic Routes를 위해 사용하는 것이다.

공식 문서

https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes

rest api에서 delete는 body가 기본적으로 없어서 이렇게 해봤다.

동일 http 요청은 header로 구분

검색해 보니 문법 갱신되기 전까지는

if(req.method === 'POST'){}
else if (req.method === 'PUT) {}

이런 식으로 짰었던 모양이다.

우린 이제 route.js를 선언하고 내부에 http 메서드들을 함수로 선언하면 자동으로 매핑이 되는 신기한 경험을 할 수 있다.

export async function GET(req, resp) {}
export async function POST(req, resp) {}
export async function PUT(req, resp) {}
export async function DELETE(req, resp) {}

근데 post에 해당하는 요청이 예를 들어 4개라면 어떻게 해야할까?

  • 별도 폴더의 route.js를 선언한다.

근데 그러면 폴더 4개 생기는 거야...? 폴더 내부에서 또 여러 crud를 한다면 몰라도 어... 내가 보기엔 그랬음.
이벤트 소싱? 무슨 책에서 본게 있는데,

동일 http 요청에 대해서는 header값으로 구분한다.

그래서 아래와 같이 적용해봤다.

import { headers } from 'next/headers'

export async function POST(req, resp) {
    const command = headers().get('command');

    console.log('jvk check command', command);  

    if(command === 'bulkInsert') {
        const db = await openDb();

nextjs에서 headers를 지원한다.
내가 command라는 헤더값을 준 걸로 분기를 태우는 것이다.

Cloudinary CDN 연동

내용이 길어져서 별도 포스팅으로 분리했다.

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

post 전송 시 formdata와 json

formdata라면 req.formData()를 사용하고 json이라면 req.json()을 사용한다.
특이점이라면 await을 필수 추가해야 한다는 점이다.

export async function POST(req, resp) {
    const data = await req.json(); // VIP:: await 해야 한다.
  
  	// formdata의 경우 아래처럼
    const formData = await req.formData();

  ...
}

공통 컴포넌트 추가

너무나도 기본인 navbar, footer 등을 추가했다. 얘네는 layout.js에 들어가면 된다.

import Navbar from "@app/components/Navbar";
import Footer from "@app/components/Footer";
import Contact from "@app/components/Contact";

import { Inter } from "next/font/google";

import "@/styles/globals.css";

const inter = Inter({ subsets: ["latin"] });

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>
  );
}

나의 경우 contact도 필수라고 생각해서 넣었음.

alias

이.. alias.... 여러번 해놓고도 또 삽질을 하다 선배 도움을 받았는데
next.js할 때 src폴더를 둘 건지 말건지 선택이 되거든... 그 부분 때문에 다른 사이트 따라할 때도 헤맸고, include 속성이 중요한데 그 부분이 잘못되어서 또 헤맸다.

지금 코드 page router를 app router로 이관하면서 alias가 @app이랑 @/로 2개다.

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "noEmit": true,
    "incremental": true,
    "module": "esnext",
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"],
      "@app/*": ["./src/app/*"],
    }
  },
  "include": ["next-env.d.js", "**/*.js", "**/*.jsx", ".next/types/**/*.js"],
  "exclude": ["node_modules"]
}

신청 기능

이거 개발하다가 잠시 멘붕이 왔었다.

신청 insert - 신청 파일 cdn 업로드 - 신청 파일 insert를 해야 하는데 비동기 처리에서 이것저것 꼬이더니 내부에서는 500에러가 났는데 응답은 200이 왔다;; 그래서 더 찾는게 힘들었다.

일단 지금부터는 에러가 생기면 하위에서 상위로 예외를 전파하는 방식으로 계속 throw err를 했더니 좀 더 에러가 잡힌다.

무엇보다 route.js에서 시작했더니 메서드가 감당할 수 없이 뚱뚱해지고 보는 내가 너무 힘들었다.
그래서 대안으로 스프링마냥 sql, mapper, service 폴더를 만들어서 다 나눠버렸다.

그리고 app router를 하면서 어디 폴더에 뭘 두어야 할지 정말 고민을 많이 했는데,

개인적으로 내린 방향은 app은 프론트와 백엔드의 라우팅 처리 관련 폴더만 두자는 것이었다.

폴더 구조가 url로 직결되는 중요한 것들은 그렇게 하고, components나 util들은 굳이 app 안에서 뚱뚱하게 있을 필요가 없다고 판단했다.

src
ㄴ app
ㄴ mapper
ㄴ service
... 기타 등등

이렇게 진행해보려 한다.
그래서 그나마 조금 꼬인 것을 풀어보았다.
부족한 것들이 또 보이니 나중에 더 해보겠다.

export async function applyCreateService(db, formData) {
  const generateUuid = uuidv4();
  formData.append("applyId", generateUuid);

  const createdApplyId = await applyInsertMapper(db, formData); // 성공

  if (createdApplyId) {
    for (const fileObj of formData.getAll("files")) {
      // formData 생성
      const cdnRegFormData = new FormData();
      cdnRegFormData.append("file", fileObj);
      cdnRegFormData.append(
        "upload_preset",
        process.env.NEXT_PUBLIC_CLOUDINARY_PRESET
      );
      cdnRegFormData.append("asset_folder", "/apply/" + formData.get("genre"));
      const createdImage = await cdnUploadImageService(cdnRegFormData);
      const applyFileRegVO = {
        ...createdImage,
        applyId: generateUuid,
      };

      console.log("createdImage:: ", createdImage);

      const fileInsertResult = await applyFileInsertMapper(db, applyFileRegVO);
      console.log("fileInsertResult", fileInsertResult);
    }
  }

  return createdApplyId;
}

ag-grid-react

회사 프로젝트에서 vue3로 ag-grid 라이브러리를 써본 적이 있다. react도 지원하니까 이걸 신청 리스트에 써보기로 했다.

npm install ag-grid-react

global css

layout.jsx에 아래를 import해준다.

// VIP:: ag-grid CSS 공통이라서 그냥 여기에 넣어둠
import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";


화면이 이렇게 있다고 치자.
신청 ID를 클릭했을 때 상세로 이동하고 싶으니 아래처럼 작성해보자.

{
      headerName: "신청 ID",
      field: "uuid",
      cellRenderer: (props) => {
        const router = useRouter();
        router.push(`/admin/apply/${props.data.uuid}`);
      },
    },

이러면 신청 리스트 들어오자마자 휘리릭 상세로 갈 수 있다. 이러지 말고 아래처럼 컴포넌트로 구현해야 한다.

cellRenderer: (props) => {
        return (
          <Link href={`/admin/apply/${props.data.uuid}`}>
            {props.data.uuid}
          </Link>
        );
      },

데이터가 제대로 표시되려면 columnDefs의 field가 데이터의 속성명과 일치해야 한다.

파라미터 받기 참 힘드네

url에서 파라미터 값 가져오는 형태 2개 있잖아.

  • /{uuid}
  • ?uuid=

/{uuid}
프론트

const ApplicationDetails = ({ params }) => {
  const { uuid } = params; 
  const { data: application, error } = useSWR(
    `http://localhost:3000/api/apply/${uuid}`,
    fetchApplyDetailPayload
  );
  
  // 이하 생략
}

export async function GET(req, { params }) {
  const { uuid } = params;
  const application = await applySelectOneService(uuid);
  return NextResponse.json({
    status: 200,
    message: "Success",
    data: application,
  });
}

{ params }라는 형태가 비슷하네. 클라이언트에서 저렇게 함수 인자로 받을 수 있는 줄 몰랐다.

?uuid=

https://nextjs.org/docs/messages/next-router-not-mounted

useRouternext/router, next/navigation으로 출처가 2개다.
app router를 쓸 거면 next/navigationuseRouter를 쓰라고 한다.

pk면 자동 not null 아니었던가

난 mySQL하면서 pk인 컬럼은 자동으로 not null과 unique가 설정된다고 알고 있었는데

sqlite3는 pk면서 nullable일 수 있었다;;

⨯ [Error: SQLITE_RANGE: column index out of range] {

파라미터 개수가 안 맞아서 생기는 이슈
db.run할 때 파라미터가 uuid 1개 넘어오는데
정작 쿼리에서 바인딩할 ?이 없을 때, 즉 파라미터 개수가 맞지 않아서도 위 이슈가 발생한다.

인터넷 검색해보면 다양한 원인이 있음.

useSWR 쓰는데 undefined of null 떠서

성능상의 이점을 위해서 useSWR을 써보려는데 이 친구는 자꾸 can not read of undefined... 그 유명한 읽을 수 없다 에러가 뜨는 것이다.

로딩 컴포넌트가 필수로 보인다. (로딩 컴포넌트 귀찮아서 tailwindcss에서 초고속 하나 만들어서 돌려쓰고 있다.)

그래서 아래처럼 처리했다. (기본 예제 수준이다)

const ApplicationDetails = ({ params }) => {
  const { uuid } = params; // VIP:: /[uuid]를 받아오는 법

  const { data: application, error } = useSWR(
    `http://localhost:3000/api/apply/${uuid}`,
    fetchApplyDetailPayload
  );

  // 에러 처리
  if (error) return <div>오류가 발생했습니다: {error.message}</div>;
  // 로딩 상태 처리
  if (!application) return <Loading />;

  return (
    <>내용....</>

이랬는데도 데이터가 null일 때 하는 처리가 시원찮다. 그냥 콘솔만 피범벅이다;;

일단 첫번째 문제는 값이 null일때 data 속성이 아예 누락되는 것이었다.
백단에서 문제가 있다.

export async function applySelectOneService(uuid) {
  const db = await openDb();
  try {
    const result = await applySelectOneMapper(db, uuid);
    const resultFiles = await applyFileSelectListMapper(db, uuid);
    return { data: result ?? null, fileList: resultFiles };
    // vip:: ?? null을 하지 않으면 data 속성 자체가 오지 않는다.
  } catch (err) {
    throw err; // VIP:: throw를 하지 않으면 윗단에서 200 응답인데 결과가 제대로 안 나오는 불상사
  } finally {
    await db.close();
  }
}
return { data: result ?? null, fileList: resultFiles };

그래서 data: null이 오는 것을 확인했는데..?
데이터를 확실히 가져온 다음에 랜더링을 해야 하는데 시점이 자꾸만 안 맞아서 문제가 생기는 듯 하다.
이 부분 로딩 로직과 데이터 NULL 구분을 고민을 많이 했었는데, 사실 간단하다.
공식을 보니 useSWR에서 isLoading을 받아서 로딩 여부를 체크할 수 있다.

  const { uuid } = params; // VIP:: /[uuid]를 받아오는 법

  const { data: application, error, isLoading } = useSWR(
    `http://localhost:3000/api/apply/${uuid}`,
    fetchApplyDetailPayload
  );

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

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

로딩중인 경우와 데이터가 존재하지 않는 경우를 위와 같이 처리해서
에러에서 탈출했다. (Loading 컴포넌트는 내가 만든 컴포넌트다)

notFound()는 next에서 지원하는 404에러를 던지는 함수다.
만들어둔 404페이지를 호출하게 된다.

import { notFound } from 'next/navigation';
profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글