React를 활용한 파일 업로드 컴포넌트 구현

박영광·2024년 2월 28일
2

React

목록 보기
12/23

실시간 업로드 진행 상황을 표시하는 파일 업로드 컴포넌트 만들기

파일 업로드는 현대 웹 애플리케이션에서 필수적인 기능 중 하나입니다. 이번 글에서는 React를 사용하여 실시간 업로드 진행 상황을 표시하는 파일 업로드 컴포넌트를 만들어보겠습니다. 스타일링과 함께 각 기능을 자세히 설명하고, 보완할 수 있는 점에 대해서도 고려해보겠습니다.

1. 파일 업로드 컴포넌트 구성하기

먼저 파일 업로드를 담당할 UploadFile 컴포넌트를 만들어보겠습니다. 이 컴포넌트는 사용자가 파일을 선택하고 업로드할 수 있는 환경을 제공할 것입니다.

import React, { useState, useRef } from "react";
import styled from "styled-components";
import axios from "axios";

/**
 * 파일 업로드 컴포넌트
 * @param {Function} onError - 업로드 중 발생한 에러 처리 함수
 * @param {Function} onUploadProgress - 업로드 진행 상황 처리 함수
 * @returns {JSX.Element} 파일 업로드 컴포넌트
 */
const UploadFile = ({ onError, onUploadProgress }) => {
  // 선택된 파일 목록과 업로드 진행 상태를 관리하는 상태 변수들
  const [selectedFiles, setSelectedFiles] = useState([]);
  const [uploadProgress, setUploadProgress] = useState(0);

  // 파일 입력(input) 요소에 대한 참조를 생성
  const fileInputRef = useRef(null);

  // 파일 선택 및 업로드 처리 함수
  const handleFileChange = async (e) => {
    // 파일 선택 시 발생하는 이벤트 핸들러
    const files = Array.from(e.target.files);
    setSelectedFiles(files);

    // 선택된 각 파일에 대해 순회하며 업로드 진행
    for (let file of files) {
      try {
        // AJAX를 통한 파일 업로드 요청
        const formData = new FormData();
        formData.append("file", file);
        await axios.post("/upload", formData, {
          // 업로드 진행 상황을 감지하여 프로그레스 바 업데이트
          onUploadProgress: (progressEvent) => {
            const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
            setUploadProgress(progress);
            onUploadProgress(progress);
          }
        });
      } catch (error) {
        // 파일 업로드 실패 시 에러 처리
        onError(error);
      }
    }
  };

  // 파일 선택 아이콘 클릭 시 파일 입력(input) 요소 클릭
  const handleIconClick = (e) => {
    e.stopPropagation(); // 부모 요소로의 이벤트 전파 차단
    fileInputRef.current.click(); // 파일 입력(input) 요소 클릭
  };

  return (
    <FileUploadWrapper>
      <UploadContainer>
      	{selectedFiles.length > 0 && (
         {/* 선택된 파일 목록을 표시 */}
      
          <FileList>
            {selectedFiles.map((file, index) => (
              <FileName key={index}>{file.name}</FileName>
             ))}
          </FileList>
        {/* 파일 선택 아이콘 */}
        <IconUploadClipContainer onClick={handleIconClick}>
          {/* 아이콘 이미지 또는 아이콘 컴포넌트 */}
          <IconUploadClip />
        </IconUploadClipContainer>
        {/* 파일 입력(input) 요소 */}
        <FileInput
          ref={fileInputRef}
          type="file"
          id="fileUpload"
          onChange={handleFileChange}
          accept="image/*"
          multiple
        />
      </UploadContainer>
        {/* 업로드 진행 상황을 나타내는 프로그레스 바 */}
        <ProgressBar style={{ width: `${uploadProgress}%` }} />
    </FileUploadWrapper>
  );
};

export default UploadFile;

// 스타일드 컴포넌트 정의
const FileUploadWrapper = styled.div`
  /* 파일 업로드 컴포넌트 전체 스타일 */
   width: 100%;
`;

const UploadContainer = styled.div`
  position: relative;
  flex: 1;
  height: 2.75rem;
  padding: 0.5rem 1rem;
  background: #f2f2f2;
  border-radius: 0.25rem;
  display: flex;
  align-items: center;
`;

const FileList = styled.div`
  /* 선택된 파일 목록에 대한 스타일 */
    margin-right: 10px;
`;

const FileName = styled.div`
  /* 파일 이름에 대한 스타일 */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
`;

const IconUploadClipContainer = styled.span`
  /* 파일 선택 아이콘에 대한 스타일 */
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
`;

const FileInput = styled.input`
  /* 파일 입력(input) 요소에 대한 스타일 */
  display: none;
`;

const ProgressBar = styled.div`
  /* 업로드 진행 상태를 나타내는 프로그레스 바에 대한 스타일 */
  position: absolute;
  bottom: 0;
  left: 0;
  width: ${uploadProgress}%;
  height: 3px;
  background-color: #007bff;
  transition: width 0.3s ease;
`;

2. 파일 목록 컴포넌트

위의 코드에선 파일목록 까지 파일 업로드 컴포넌트에 담았지만 만약 따로 파일 목록을 컴포넌트화 한다면

/**
 * 파일 목록 컴포넌트
 * @param {Array} files - 선택된 파일 목록
 * @returns {JSX.Element} 파일 목록 컴포넌트
 */
const FileList = ({ files }) => (
  <FileListContainer>
    {/** 선택된 파일 목록을 표시 */}
    {files.map((file, index) => (
      <FileName key={index}>{file.name}</FileName>
    ))}
  </FileListContainer>
);

// 파일 목록 컴포넌트의 스타일드 컴포넌트 정의
const FileListContainer = styled.div`
  /* 파일 목록 컴포넌트 전체 스타일 */
`;

// 파일 이름에 대한 스타일드 컴포넌트 정의
const FileName = styled.div`
  /* 파일 이름에 대한 스타일 */
`;

으로 만들 수 있겠습니다.

위 코드는 다음과 같은 주요 기능과 보완점을 가지고 있습니다:

  1. 파일 업로드 및 진행 상황 표시: handleFileChange 함수를 통해 파일을 선택하고 업로드하며, 진행 상황은 프로그레스 바로 표시됩니다. 프로그레스 바는 업로드 상태에 따라 동적으로 변화합니다.

  2. 파일 선택 아이콘 기능: 파일 선택 아이콘을 클릭하면 실제 파일 선택 창이 열리도록 처리됩니다.

  3. 파일 목록 표시: 선택된 파일 목록이 화면에 표시됩니다. 각 파일의 이름은 가로로 표시되며, 너무 긴 파일 이름은 생략됩니다.

  4. 스타일링: 각 요소들은 styled-components를 활용하여 스타일이 적용되었습니다. 이를 통해 컴포넌트의 외관과 레이아웃을 손쉽게 조정할 수 있습니다.

  5. 보완점: 파일 목록이 가로로 너무 길어질 경우 화면 크기를 넘어가는 문제가 있습니다. 이를 해결하기 위해 파일 이름이 적당한 너비를 가지고 자동으로 줄 바꿈되도록 스타일을 추가하는 것이 좋습니다.

  6. 전체 코드 및 부모 컴포넌트 활용: 이 컴포넌트는 부모 컴포넌트에서 사용될 것을 가정하고 작성되었습니다. 부모 컴포넌트에서는 UploadFile 컴포넌트를 렌더링하고, 필요한 콜백 함수들을 props로 전달하여 사용할 수 있습니다.

3. 사용되는 부모 컴포넌트

부모 컴포넌트에서의 사용 예시는 다음과 같습니다

const ParentComponent = () => {
  const handleError = (error) => {
    // 에러 처리 로직
  };

  const handleProgress = (progress) => {
    // 업로드 진행 상황 처리 로직
  };

  return (
    <div>
      <h1>파일 업로드</h1>
      <UploadFile onError={handleError} onUploadProgress={handleProgress} />
    </div>
  );
};

위와 같이 부모 컴포넌트에서 UploadFile 컴포넌트를 사용할 수 있습니다. 필요에 따라 에러 처리 및 업로드 진행 상황 처리 함수를 구현하고 전달하여 사용할 수 있습니다.


위의 코드들을 나눠서 설명해보자면

1) UploadFile 컴포넌트


import React, { useState, useRef } from "react";
import styled from "styled-components";
import axios from "axios";

const UploadFile = ({ onError, onUploadProgress }) => {
  const [selectedFiles, setSelectedFiles] = useState([]);
  const fileInputRef = useRef(null);
  const [uploadProgress, setUploadProgress] = useState(0);

  // 파일 선택 및 업로드 처리 함수
  const handleFileChange = async (e) => {
    const files = Array.from(e.target.files);
    setSelectedFiles(files);

    for (let file of files) {
      try {
        // 파일 업로드 AJAX 요청
        const formData = new FormData();
        formData.append("file", file);
        await axios.post("/upload", formData, {
          onUploadProgress: (progressEvent) => {
            const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
            setUploadProgress(progress);
            onUploadProgress(progress);
          }
        });
      } catch (error) {
        onError(error);
      }
    }
  };

  // 파일 선택 아이콘 클릭 시 처리하는 함수
  const handleIconClick = (e) => {
    e.stopPropagation();
    fileInputRef.current.click();
  };

  return (
    <FileUploadWrapper>
      {/* 파일 목록 및 아이콘 */}
      {/* 프로그레스 바 */}
    </FileUploadWrapper>
  );
};

export default UploadFile;

2) 파일 목록 및 업로드 아이콘 표시하기

return (
  <UploadContainer>
    {selectedFiles.length > 0 && (
      <FileList>
        {selectedFiles.map((file, index) => (
          <FileName key={index}>{file.name}</FileName>
        ))}
      </FileList>
    )}
    <IconUploadClipContainer onClick={handleIconClick}>
      {/* 업로드 아이콘 */}
    </IconUploadClipContainer>
    <FileInput
      ref={fileInputRef}
      type="file"
      id="fileUpload"
      onChange={handleFileChange}
      accept="image/*"
      multiple
    />
  </UploadContainer>
);

3) 프로그레스 바 표시하기

return (
  <FileUploadWrapper>
    {/* 파일 목록 및 아이콘 */}
    {/* 프로그레스 바 */}
    {uploadProgress > 0 && <ProgressBar />}
  </FileUploadWrapper>
);

4) 파일 업로드 진행 상황 표시 및 에러 처리

// 파일 선택 및 업로드 처리 함수
const handleFileChange = async (e) => {
  const files = Array.from(e.target.files);
  setSelectedFiles(files);
  setUploadProgress(0); // 업로드 진행 상황 초기화

  for (let file of files) {
    try {
      // 파일 업로드 AJAX 요청
      const formData = new FormData();
      formData.append("file", file);
      await axios.post("/upload", formData, {
        onUploadProgress: (progressEvent) => {
          const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
          setUploadProgress(progress);
          onUploadProgress(progress);
        }
      });
    } catch (error) {
      onError(error); // 에러 처리
    }
  }
};

5) 스타일드 컴포넌트 정의

const FileUploadWrapper = styled.div`
  width: 100%;
`;

const UploadContainer = styled.div`
  position: relative;
  flex: 1;
  height: 2.75rem;
  padding: 0.5rem 1rem;
  background: #f2f2f2;
  border-radius: 0.25rem;
  display: flex;
  align-items: center;
`;

const FileList = styled.div`
  margin-right: 10px;
`;

const FileName = styled.div`
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
`;

const IconUploadClipContainer = styled.span`
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
`;

const FileInput = styled.input`
  display: none;
`;

const ProgressBar = styled.div`
  position: absolute;
  bottom: 0;
  left: 0;
  width: ${uploadProgress}%;
  height: 3px;
  background-color: #007bff;
  transition: width 0.3s ease;
`;

4. 보완사항 및 추가 고려 사항

  • 현재는 파일 선택 후 즉시 업로드가 이루어지지만, 선택한 파일들의 유효성을 먼저 검사하여 필요한 경우에만 업로드를 수행하도록 개선할 수 있습니다.

  • 사용자 경험을 향상시키기 위해 파일 업로드 상태에 따라 UI를 보다 풍부하게 표현할 수 있습니다. 예를 들어, 업로드 완료 시에는 성공 메시지를 표시하거나, 에러 발생 시에는 에러 메시지와 함께 재시도 버튼을 제공할 수 있습니다.

  • 스타일링을 더욱 개선하여 사용자가 직관적으로 파일 업로드 영역을 인식할 수 있도록 할 수 있습니다. 예를 들어, 드래그 앤 드롭 기능을 추가하거나, 마우스 오버 효과를 적용할 수 있습니다.


결론

이렇게 파일 업로드에 대해 알아 보는 과정을 통해 프로젝트에 필요한 파일 업로드 기능을 구현하는 데 도움이 되기를 바랍니다.

profile
매일 1mm씩 성장하겠습니다

0개의 댓글