티스토리 뷰

reactJS

reactJS + springboot 파일 업로드 구현하기 (1)

코딩하는 둥아 2022. 4. 26. 14:33
728x90

✅ 개발 환경

Mac M1 노트북

React + Typescript

SpringBoot + JPA

  - JDK 1.8 /  Language Level 8

mySqk 사용 (로컬에서 돌렸습니다)

IntelliJ 사용

 

React와 SpringBoot 환경이 모두 세팅되어 있다는 가정 하에 시작하겠습니다!

저는 이번에 JPA를 사용해보고 싶어서 사용했습니다 🙂

 

저는 아래에 첨부된 링크를 참고하여 환경을 세팅했습니다.

 

React는 디폴트 3000번 포트를 사용하였고,

SpringBoot는 8082번 포트를 사용했습니다.

 

✅ JPA란?

- JPA는 ORM을 사용하기 위한 인터페이스를 모아둔 것이라고 볼 수 있다.

- 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다.

   여기서 ORM이란 Object-Relational Mapping으로, 객체와 DB 테이블이 매핑을 이루는 것을 의미한다.

- JPA는 결국 인터페이스이므로, 이를 사용하기 위해서는 JPA를 구현한 프레임워크가 필요하다

- Hibernate, EclipseLink 와 같은 ORM 프레임워크를 사용한다.

   JPA를 구현한 ORM 프레임워크, JPA 명세의 구현체를 의미함

 

✅  React에서 파일 업로드 구현하기

우선 아래에 풀 코드가 있으니 참고해주세요!

 

일단 jsx 코드 부분입니다.

간단하게 file을 입력받는 input과, 

화면에서 업로드한 이미지를 미리보는 부분입니다.

return (
        <div>
            <h2>사진 업로드</h2>
            <input type="file" id="file" onChange={handleChangeFile} multiple/>
            <h3>업로드 한 사진 미리보기</h3>
            {imgBase64.map((item) => {
                return (
                    <img
                        key={item}
                        src={item}
                        alt={"First slide"}
                        style={{width:"200px", height:"150px"}}
                    />
                )
            })}
            <button onClick={WriteBoard} style={{border: '2px solid black'}}>이미지 업로드</button>
        </div>
    );

업로드 전 / 업로드 후

 

input에 사진을 업로드하면 handleChangeFile 함수가 실행됩니다.

입력받은 사진 파일을 imgFile 변수에 저장하고,

파일을 비트맵 데이터 파일로 변환합니다.

변환된 비트맵 데이터는 화면에서 업로드한 사진의 미리보기에 사용됩니다.

 

현재 프론트에서는 multiple input이 가능한데, backend에서는 한 장만 처리하도록 구현해뒀습니다!

const handleChangeFile = (event: any) => {
        console.log(event.target.files);
        setImgFile(event.target.files);
        setImgBase64([]);
        for(let i=0 ; i<event.target.files.length ; i++) {
            if(event.target.files[i]) {
                let reader = new FileReader();
                reader.readAsDataURL(event.target.files[i]);
                reader.onloadend = () => {
                    const base64 = reader.result; // 비트맵 데이터 리턴, 이 데이터를 통해 파일 미리보기가 가능함
                    console.log(base64)
                    if(base64) {
                        let base64Sub = base64.toString()
                        setImgBase64(imgBase64 => [...imgBase64, base64Sub]);
                    }
                }
            }
        }
    }

이미지 업로드 버튼을 클릭하면 WriteBoard 함수가 실행됩니다.

입력받은 이미지 파일 imgFile을 FormData 형식으로 바꿉니다.

comment와 같은 형식으로 다른 데이터를 FormData에 함께 덧붙일 수 있습니다.

 

그리고 axios를 사용하여 post request를 보냅니다.

여기서 저는 custom hook을 생성하였습니다.

const WriteBoard = async () => {
        const fd = new FormData();
        for(let i=0 ; i<imgFile.length ; i++) {
            fd.append("file", imgFile[i]);
        }
        // 안돌아감.
        // Object.values(imgFile).forEach((file) => {
        //     fd.append("file", file as Blob)
        // });

        fd.append(
            "comment",
            "hello world"
        );

// custom hook
        await fileInstance({
            method: 'post',
            url: '/api/file/image',
            data: fd
        })
        .then((response) => {
            if(response.data) {
                setImgFile(null);
                setImgBase64([]);
                alert("업로드 완료!");
            }
        })
        .catch((error) => {
            console.log(error)
            alert("실패!");
        })
    }

 

👉 useAxiosLoader.ts Full code

아래와 같이 axios를 모듈화하였습니다.

모듈화하지 않는 경우, axios 요청을 보낼 때마다 header를 작성하고 baseURL을 지정해줘야 합니다.

하지만 커스텀하여 사용하면 그러한 수고를 덜 수 있습니다!

form-data인 경우에 사용하는 fileInstance, application/json 타입일 때 instance를 생성하였습니다.

 

baseURL에는 서버의 기본 URL인 http://localhost:8082를 지정하였습니다.

instance.interceptors.request.use ~ 부분은 axios 인터셉터 부분입니다.

저는 아직 로그인 기능이 없어 비워놨지만, 여기서 토큰이나 세션을 확인하는 등의 작업을 수행합니다.

import axios, { AxiosInstance } from 'axios';

const instance: AxiosInstance = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    },
});

const fileInstance: AxiosInstance = axios.create({
    baseURL: 'http://localhost:8082',
    headers: {
        'Content-Type': 'multipart/form-data;'
    },
});

instance.interceptors.request.use(
    config => {
        // 요청을 보내기 전에 수행할 로직
        return config;
    },
    error => {
        console.log(error)
        return Promise.reject(error);
    }
)

instance.interceptors.response.use(
    config => {
        // 응답에 대한 로직
        return config;
    },
    error => {
        console.log(error)
        return Promise.reject(error);
    }
)

export {instance, fileInstance};

 

👉 main.tsx Full code

import React, {useState, useEffect} from 'react';
import {instance, fileInstance} from "../hooks/useAxiosLoader";

function MainPage() {
    const [imgBase64, setImgBase64] = useState([]);
    const [imgFile, setImgFile] = useState(null);


    const handleChangeFile = (event: any) => {
        console.log(event.target.files);
        setImgFile(event.target.files);
        setImgBase64([]);
        for(let i=0 ; i<event.target.files.length ; i++) {
            if(event.target.files[i]) {
                let reader = new FileReader();
                reader.readAsDataURL(event.target.files[i]);
                reader.onloadend = () => {
                    const base64 = reader.result; // 비트맵 데이터 리턴, 이 데이터를 통해 파일 미리보기가 가능함
                    console.log(base64)
                    if(base64) {
                        let base64Sub = base64.toString()
                        setImgBase64(imgBase64 => [...imgBase64, base64Sub]);
                    }
                }
            }
        }
    }

    const WriteBoard = async () => {
        const fd = new FormData();
        for(let i=0 ; i<imgFile.length ; i++) {
            fd.append("file", imgFile[i]);
        }
        // 안돌아감.
        // Object.values(imgFile).forEach((file) => {
        //     fd.append("file", file as Blob)
        // });

        fd.append(
            "comment",
            "hello world"
        );

        await fileInstance({
            method: 'post',
            url: '/api/file/image',
            data: fd
        })
        .then((response) => {
            if(response.data) {
                console.log(response.data)
                readImages();
                setImgFile(null);
                setImgBase64([]);
                alert("업로드 완료!");
            }
        })
        .catch((error) => {
            console.log(error)
            alert("실패!");
        })
    }


    return (
        <div>
            <h2>사진 업로드</h2>
            <input type="file" id="file" onChange={handleChangeFile} multiple/>
            <h3>업로드 한 사진 미리보기</h3>
            {imgBase64.map((item) => {
                return (
                    <img
                        key={item}
                        src={item}
                        alt={"First slide"}
                        style={{width:"200px", height:"150px"}}
                    />
                )
            })}
            <button onClick={WriteBoard} style={{border: '2px solid black'}}>이미지 업로드</button>
        </div>
    );
}

export default MainPage;

 

 

✅ SpringBoot에서 파일 업로드 구현하기

FileController.java, FileEntity, FileRepostiory 세 개의 파일이 필요합니다.

 

⭐️ application.properties

그리고 파일의 최대 업로드 용량 지정, 이미지 경로 지정 등의 추가적인 설정을 application.properties에 해줍니다.

여기서 database 연결과 JPA와 관련한 설정 또한 진행하게 됩니다.

server.address=localhost
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/TEST_DB?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=test_user
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

# 로깅 레벨
logging.level.org.hibernate=info 

# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true

spring.servlet.multipart.maxFileSize=20MB
spring.servlet.multipart.maxRequestSize=20MB

spring.web.resources.static-locations=classpath:/resources/,classpath:/static/

 

⭐️ FileController.java

filepath 변수는 제가 이미지를 저장한 폴더의 경로입니다.(프로젝트 static/images 폴더 밑)

이름이 중복되는 경우를 막기 위해 currentTimeMillis()를 붙여 safeFile 명을 생성합니다.

files[0].transferTo(f1) 부분이 실질적으로 지정한 경로에 파일을 저장하는 부분입니다.

 

final FileEntity file = FileEntity.builder()
        .filename(safeFile)
        .build();

그리고 FILE 테이블에 filename을 저장합니다.

파일을 불러올 때 사용할 정보입니다.

package com.example.demo.controller;

import com.example.demo.entity.FileEntity;
import com.example.demo.repo.FileRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/file")
public class FileController {
    private final FileRepository fileRepository;
    String filepath = "/Users/evelyn/Desktop/demo/src/main/resources/static/images/";

    /**
     *  이미지 업로드
     * @return Map<String, Object>
     */
    @PostMapping("image")
    public FileEntity uploadImage(HttpServletRequest request,
                                  @RequestParam(value="file", required = false) MultipartFile[] files,
                                  @RequestParam(value="comment", required = false) String comment) {

        String FileNames = "";

        String originFileName = files[0].getOriginalFilename();
        long fileSize = files[0].getSize();
        String safeFile = System.currentTimeMillis() + originFileName;

        File f1 = new File(filepath + safeFile);
        try {
            files[0].transferTo(f1);
        } catch (IOException e) {
            e.printStackTrace();
        }

        final FileEntity file = FileEntity.builder()
                .filename(safeFile)
                .build();

        return fileRepository.save(file);
    }

}

FILE table 조회 결과

 

⭐️ FileRepostiroy.java

package com.example.demo.repo;

import com.example.demo.entity.FileEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface FileRepository extends JpaRepository<FileEntity, Long> {

}

 

⭐️ FileEntity.java

create_dt의 경우 default값으로 현재 시간을 넣어줄 것이므로, 아래의 Definition을 해줍니다.

columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
package com.example.demo.entity;

import lombok.*;

import javax.persistence.*;
import java.util.Date;

@Getter // getter 메소드 생성
@Builder // 빌더를 사용할 수 있게 함
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name="file") // 테이블 명을 작성
public class FileEntity {
    @Id // primary key임을 명시
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long pid;

    @Column(nullable = false, unique = true, length = 1000)
    private String filename;

    @Column(nullable = false, updatable = false, insertable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private String created_dt;
}

여기까지 하면 프론트와 백의 파일 업로드 구현이 완료되었습니다.

 

✅ CORS 에러 발생?

그런데 http://localhost:3000/main => http://localhost:8082/api/file/image 로 request를 보내기 때문에

CORS 정책에 위반된다는 에러가 등장할 것입니다.

 

그러면 springboot의 application을 열어서 addCorsMappings를 설정해주면 됩니다.

혹은 react에서 proxy를 설정하는 방법도 있습니다!

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class DemoApplication implements WebMvcConfigurer {

    // CORS 설정
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**").allowedOrigins("*");
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

 

여기까지 하면 파일 업로드 완성입니다!

다음 포스팅에서는 이미지 불러오기 & 이미지 다운로드를 구현해보겠습니다 😉

 

 

✅ 참고 링크

- Mac Mysql 세팅하기

https://memostack.tistory.com/150#toc-%EC%84%A4%EC%A0%95%205:%20%EB%B3%80%EA%B2%BD%20%EB%82%B4%EC%9A%A9%EC%9D%84%20%ED%85%8C%EC%9D%B4%EB%B8%94%EC%97%90%20%EC%A0%81%EC%9A%A9%20%EC%97%AC%EB%B6%80

- React 파일 업로드 구현하기

https://cookinghoil.tistory.com/114

- JPA란?

https://goddaehee.tistory.com/209

- SpringBoot JPA 환경 구축하기

https://memostack.tistory.com/155#toc-%EA%B2%B0%EA%B3%BC%20%ED%99%95%EC%9D%B8

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함