React와 Node.js를 활용한 PDF 생성 및 미리보기 이미지 처리(1)

이번 포스팅은 파일을 업로드하고, 업로드한 파일 리스트를 받아와서 미리보기 이미지를 생성하고, 이미지를 클릭하면 PDF 파일을 다운로드 받을 수 있는 기능을 구현하는 방법에 대해 알아본다. 클라이언트는 React를 사용하고, 서버는 Node.js를 사용하여 구현하였다.

필자는 프론트엔드 개발자이므로, 백엔드는 Node.js를 배워서 처음 구현해보는 부분이다. 평소 파일 업로드 기능을 구현할때 프론트와 백엔드에서 어떻게 주고 받아지는가에 대한 로직이 궁금했었는데 이번 기회에 확실히 정리해보고자 한다.

전체 로직 흐름을 한눈에


코드로 파악한 플로우를 피그마를 사용하여 그림으로 그려보았다. 필자는 코딩을 처음 배울때부터 변수를 그릇으로 표현하며 코드 한줄 한줄을 그림으로 그려 이해한 사람이다 🤭

그림으로 플로우를 그려보면 흐름을 한눈에 파악하기 좋고, 다른사람에게 설명할때도 듣는 사람이 이해하기 좋기 때문에 항상 그림을 그려서 정리해 놓는 편이다.

아래 그림을 통해 파일 업로드를 처리하는 과정에 대해 이해해보자.

  1. 클라이언트에서 파일을 선택하고, 서버로 전송한다.
  2. 서버에서 파일을 읽어서 PDF 파일을 생성한다.
  3. 서버에서 파일을 읽어서 미리보기 이미지를 생성한다.
  4. 서버에서 파일을 받아서 저장한다.
  5. 서버에서 파일 리스트를 클라이언트로 전송한다.
  6. 클라이언트에서 파일 리스트를 받아서 미리보기 이미지를 렌더링한다.

전체적인 흐름을 파악했다면, 코드로 확인해보자.

Client 파일 업로드 API 요청


Server와 통신을 위한 라이브러리로 react-query를 사용했다. 다른 라이브러리에 비해 월등한 장점을 가지고 있다고해서 사용했고, 나중에 react-query에 대한 자세한 포스팅을 올려보기로 한다.

import { useQuery } from "@tanstack/react-query";
// ...
import styles from "./styles.module.scss";

export type BookType = {
  //  ...
};

export default function Ebook() {
  const { selectedFile, setSelectedFile } = useEbook();
  const [searchText, setSearchText] = useState("");

  const {
    isLoading,
    error,
    data: books,
    refetch,
  } = useQuery<BookType[]>({
    queryKey: ["books"],
    queryFn: getEbooks,
  });

  const handleSearch = () => {
    //  ...
  };

  const handleFileUpload = async () => {
    if (selectedFile == null) return;

    const formData = new FormData();

    formData.append("file", selectedFile);

    formData.append("filename", selectedFile.name);
    formData.append("type", selectedFile.type);
    formData.append("size", selectedFile.size.toString());
    formData.append("username", "roxie");
    formData.append("name", "Roxie");

    try {
      await postEbook(formData);
      setSelectedFile(null);
      refetch(); // Refetch the books after successful upload
    } catch (error) {
      // onError(error);
      console.log(error);
    }
  };

  if (isLoading) return <Loading />;
  if (error) return <div>Error loading books: {error.message}</div>;

  return (
    <article className={styles.ebook}>
      <div className={styles.file_drag}>
        // serach input ...
        <></>
        // file upload component
        <FileDropZone accept=".pdf" desc="*PDF 파일만 등록 가능합니다." />
        // file upload button
        <NormalButton
          title="Upload file"
          type="save"
          onClick={handleFileUpload}
          disabled={!selectedFile}
        />
      </div>
      <div
        className={`${styles.books_wrapper} ${
          !searchResults || searchResults.length === 0
            ? styles.books_no_results
            : ""
        }`}
      >
        <BooksList books={searchResults} />
      </div>
    </article>
  );
}

파일을 업로드 할때, 왜 FormData를 사용해야 할까?


파일을 업로드 할 때, 일반 적으로 사용되는 FormData 객체에 대해 짚고 넘어가지 않을 수 없다. 본격적인 구현에 앞서 FormData 객체를 사용하는 이유에 대해 알아보자.

FormData는 파일 데이터를 서버로 전송하는 데 일반적인 JSON 형식으로는 파일 데이터를 전송할 수 없기 때문에 FormData를 사용한다.

다양한 데이터 타입 처리

FormData는 파일뿐만 아니라 텍스트 같은 여러 타입의 데이터를 함께 전송할 수 있다. 파일(file)과 문자열 데이터(username)를 FormData에 추가하여 함께 서버로 전송하는 예시를 보면 formData객체에서 지원해주는 append()를 사용하여 다양한 타입의 데이터를 함께 묶어 api요청을 할 수 있다. 이는 파일 업로드와 관련된 추가 정보를 서버로 전송하는 데 유용하다.

const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');

// 파일 추가
formData.append("file", fileInput.files[0]);

// 추가 텍스트 데이터 추가
formData.append("username", "JohnDoe");

// fetch로 API 호출
fetch("/upload", {
  method: "POST",
  body: formData,
})
  .then((response) => response.json())
  .then((data) => console.log("File upload successful", data))
  .catch((error) => console.error("Error uploading file:", error));

멀티파트 데이터 전송 지원

FormData 객체를 사용하면 브라우저가 자동으로 multipart/form-data 형식을 설정 하기때문에 Content-Type 헤더를 명시적으로 설정할 필요가 없다.

파일 업로드의 유연성

FormData는 여러 개의 파일과 메타데이터를 유연하게 추가할 수 있어, 다중 파일 업로드를 처리할 때 유용하다.

const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');

// 여러 개의 파일 추가
for (let i = 0; i < fileInput.files.length; i++) {
  formData.append("files[]", fileInput.files[i]);
}

// 파일에 대한 추가 정보(메타데이터) 추가
formData.append("uploadTime", new Date().toISOString());

// fetch로 API 호출
fetch("/upload", {
  method: "POST",
  body: formData,
})
  .then((response) => response.json())
  .then((data) => console.log("Multiple files uploaded", data))
  .catch((error) => console.error("Error uploading files:", error));

이러한 이유로 FormData는 파일 업로드를 처리하는 데 매우 유용한 도구이며, 파일 업로드 기능을 구현할 때는 FormData를 적극적으로 활용하여 효율적이고 안전한 파일 업로드 솔루션을 구축할 수 있다.

여기까지, Client에서 FormData를 사용하여 Server에 API요청하는 방법과 전체적인 흐름에 대해 알아보았다. 다음 포스팅에서 Server에서 파일을 어떻게 처리하는지에 대해 알아보자.

React Node.js PDF File Upload FormData