심화프로젝트 - 코드개선하기

ㅇㅖㅈㅣ·2024년 3월 19일
0

Today I Learned

목록 보기
78/93
post-thumbnail

심화프로젝트때 진행했던 Keyboduck 에서 내가 담당했던 부분의 코드를 다시 보면서 중복되는 부분이나 쿼리 부분을 분리하는 작업을 해보았다.

📝 댓글 CRUD 코드 개선

리뷰페이지의 댓글 컴포넌트안에 한줄로 쭉 나열되어있던 코드에서 CRUD하는 쿼리 useMutation 부분을 Custom hook으로 분리해주는 작업을 하였다.

// useReviewComment.ts
import {queryClient} from '@/pages/_app';
import {
  addReviewComment,
  deleteReviewComment,
  fetchReviewComment,
  updateReviewComment,
} from '@/pages/api/review-comment';
import {useMutation, useQuery} from '@tanstack/react-query';
import {useAlertMessage} from './useAlertMessage';

interface UserInfo {
  id: string;
}

const useReviewComment = (
  reviewId: number,
  authorId: string,
  userInfo: UserInfo,
  title: string,
  userId: string,
  comment: string,
) => {
  const {addAlertMessage} = useAlertMessage();

  const {data: reviewCommentData} = useQuery({
    queryKey: ['fetchReviewCommentList'],
    queryFn: fetchReviewComment,
    refetchOnWindowFocus: false,
    staleTime: 3000,
  });

  const addCommentMutate = useMutation({
    mutationFn: async () => await addReviewComment(userId, comment, reviewId),
    onSuccess: () => {
      queryClient.invalidateQueries({queryKey: ['fetchReviewCommentList']});

      if (authorId !== userInfo.id) {
        addAlertMessage({
          type: 'comment',
          message: `작성하신 리뷰 ${title} 에 댓글이 달렸습니다.`,
          userId: authorId,
          targetId: reviewId,
        });
      }
    },
  });

  const deleteCommentMutate = useMutation({
    mutationFn: deleteReviewComment,
    onSuccess: () => {
      queryClient.invalidateQueries({queryKey: ['fetchReviewCommentList']});
    },
  });

  const updateCommentMutate = useMutation({
    mutationFn: updateReviewComment,
    onSuccess: () => {
      queryClient.invalidateQueries({queryKey: ['fetchReviewCommentList']});
    },
  });
  return {
    reviewCommentData,
    addCommentMutate,
    deleteCommentMutate,
    updateCommentMutate,
  };
};

export default useReviewComment;

🏙️ 이미지 업로드 관련 중복코드 개선

프로젝트 당시에는 기능구현이 우선적인 목표였기 때문에 리뷰를 작성하는 페이지와 수정하는 페이지에서 똑같이 사용되는 중복코드가 많았었다.
이 부분의 코드를 줄이기 위해 Custom hook으로 분리하는 작업을 진행해보았다.

이미지 드래그 앤 드롭, 업로드, 삭제 관련된 코드

// useImageUpload.ts
import {useToast} from '@/hooks/useToast';
import {useState, useEffect} from 'react';

const useImageUpload = () => {
  const [imageFile, setImageFile] = useState<string[]>([]);
  const {warnTopCenter} = useToast();

  // 이미지 파일 미리보기, 최대5장
  const processImageFiles = (files: FileList, existingImageFiles: string[]): string[] => {
    let imageFiles: string[] = [...existingImageFiles];

    for (let i = 0; i < files.length; i++) {
      const file: File = files[i];
      const reviewImageUrl: string = URL.createObjectURL(file);
      imageFiles.push(reviewImageUrl);
    }

    if (imageFiles.length > 5) {
      warnTopCenter({message: '최대 5장 까지만 업로드 할 수 있습니다', timeout: 2000});
      imageFiles = imageFiles.slice(0, 5);
    }
    return imageFiles;
  };

  // 이미지 드래그 앤 드롭으로 가져오기
  const handleDragOver = (event: React.DragEvent<HTMLLabelElement>) => {
    event.preventDefault();
  };

  const handleDrop = (event: React.DragEvent<HTMLLabelElement>) => {
    event.preventDefault();

    if (!event.dataTransfer.files) return;

    const droppedFiles: FileList = event.dataTransfer.files;
    const processedImageFiles = processImageFiles(droppedFiles, imageFile);
    setImageFile(processedImageFiles);
  };

  // 이미지 클릭해서 업로드하기
  const imageUploadHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (!event.target.files) return;

    const files: FileList = event.target.files;
    const processedImageFiles = processImageFiles(files, imageFile);
    setImageFile(processedImageFiles);
  };

  // 이미지 삭제
  const imageDeleteHandler = (index: number) => {
    const updatedImageFiles = [...imageFile];
    updatedImageFiles.splice(index, 1);
    setImageFile(updatedImageFiles);
  };

  // blob형태를 url로 변환
  const fetchImageFile = async (blobUrl: string): Promise<File> => {
    const response = await fetch(blobUrl);
    const blob = await response.blob();
    return new File([blob], 'upload.png', {type: 'image/png'});
  };

  useEffect(() => {
    // 컴포넌트 언마운트 시 URL 객체 해제
    return () => {
      imageFile.forEach(url => URL.revokeObjectURL(url));
    };
  }, [imageFile]);

  return {
    imageFile,
    setImageFile,
    handleDragOver,
    handleDrop,
    imageUploadHandler,
    imageDeleteHandler,
    fetchImageFile,
  };
};

export default useImageUpload;

useEffect로 컴포넌트가 언마운트 될 때 URL 객체가 해제될 수 있는 코드가 추가되었는데 사용하는 이유는 아래와 같다.

일반적으로 이미지 파일을 URL.createObjectURL을 통해 생성한 후 사용하면, 해당 URL은 해당 이미지 파일의 객체에 대한 참조를 유지한다. 이는 브라우저 메모리에 임시로 저장되는데, 컴포넌트가 언마운트 되어 해당 이미지 파일이 화면에서 사라지더라도 URL 객체는 메모리에 유지된다. 이렇게 URL 객체가 계속 유지되면 메모리 누수가 발생할 수 있다.


따라서 컴포넌트가 언마운트 될 때 imageFile 배열에 있는 모든 URL 객체를 해제해주는 것이 좋은데 useEffect의 cleanup 함수를 활용하였다.
cleanup 함수는 해당 useEffect의 종속성 배열이 변경되어 cleanup이 필요할 때 실행된다.


즉, imageFile 배열에 있는 모든 URL 객체를 컴포넌트가 언마운트 될 때 해제해주어 불필요한 메모리 사용을 방지하고 메모리 누수를 예방할 수 있다.

이 코드를 빠뜨리면 브라우저 성능 저하와 메모리 부족으로 이어질 수 있으므로 주의해야 한다!

참고
MDN
블로그

profile
웰씽킹_나는 경쟁력을 갖춘 FE개발자로 성장할 것이다.

0개의 댓글