3. 파일 전송하기

3. 파일 전송하기

백엔드와 어떻게 통신해야 할까?

채점 서비스의 첫 번째 단계는 사용자의 파일을 받아 백엔드로 넘겨주는 일.

react의 확장성으로 대표되는 다양한 써드 파티 라이브러리의 힘이 발휘되는 순간이다.



Axios 사용하기

Axios란, 브라우저, NodeJS를 위한 HTTP 비동기식 통신 라이브러리다.

풀어 쓰자면, 특정 task를 수행하면서 block하지 않는 HTTP 비동기식 통신을 지원한다.

POST, GET과 같은 HTTP 프로토콜을 사용해 데이터를 주고받을 수 있다.


내가 필요한 파일 업로드는 받은 파일을 HTTP 프로토콜을 이용해 백엔드로 전송해야 하므로,

POST 메소드를 이용해야 하며, 매개변수로 클래스룸 인증 토큰과 사용자가 입력한 학번을 같이 넘겨줘야 한다.



// file: 'FileTransfer.js'
function FileUploadComponent (props) {
    const classes = useStyles();
    const [file, setfile] = useState(null);
    
    const fileUpload = (file) => {
        const formData = new FormData();
        formData.append('file', file);

        return axios.post('/api/grade/execute', formData, {
        
          ...
        
        }
    }
    
}

Axios 문서를 정독하고 예제 코드를 참고하면서 스스로 작성했는데,

파악한 구조는 다음과 같다.


// file: 'FileTransfer.js'
    const [file, setfile] = useState(null);
    
    const fileUpload = (file) => {
        const formData = new FormData();
        formData.append('file', file);

        ...
        
    }
    
}


React hook을 이용해 상수인 file을 업데이트할 것이다.

FormData() 생성자는 key-value 쌍 객체인 FormData를 만든다.

fileUpload 함수는 파일을 매개변수로 받고, FormData에 file 키에 값으로 추가한다.



이전에 매개변수로 학번과 인증 토큰을 넘겨줘야 하므로 추가한다. (params 키로 추가할 수 있다)

// file: 'FileTransfer.js'
    const [file, setfile] = useState(null);
    
    const fileUpload = (file) => {
        const formData = new FormData();
        formData.append('file', file);

        return axios.post('/api/grade/execute', formData, {
            params: {
                studentNum: props.id,
                token: props.name,
            },
        
        ...
    }
    
}


POST 메소드를 사용하고, 백엔드에 미리 지정한 경로를 입력해야 백엔드 쪽에서 데이터를 받을 수 있다.

file을 추가한 FormData를 매개변수로 주고, params를 추가한다.



그리고 중요한 것! headers에 넘겨주는 데이터 타입이 무엇인지 명시해줘야 했다.

// file: 'FileTransfer.js'
    const [file, setfile] = useState(null);
    
    const fileUpload = (file) => {
        const formData = new FormData();
        formData.append('file', file);

        return axios.post('/api/grade/execute', formData, {
            params: {
                studentNum: props.id,
                token: props.name,
            },
            
            headers: {
                'Content-Type': 'multipart/form-data'
            }
    }
    
}

파일일 경우 multipart/form-data를 타입으로 지정한다.

만약 JSON 형식일 경우, application/json 타입이다.

많이 쓸 타입이니 알아두면 좋을 듯😆



이제 업로드 함수는 완성했으니, 업로드 함수도 만들어보자.


// file: 'FileTransfer.js'
    const upload = (e) => {
        e.preventDefault();
        fileUpload(file)
    };
}


업로드 버튼을 눌렀을 때 핸들링하기 위한 upload 함수를 정의했다.

e는 event로, e.preventDefault() 함수를 사용해 준 이유는 업로드 버튼을 눌렀을 때 창을 새로 그리지 않기 위해서.

새로고침, 이동을 막아줘야 사용자에게 업로드 후 성공 / 실패 여부를 알려줄 수 있기 때문이다.



이제 이 업로드 및 전송 역할을 하는 버튼 컴포넌트를 구성하면 된다!


// file: 'FileTransfer.js'
    return (
        <div>
            <form onSubmit={upload} className={classes.form}>
                <input accept="application/zip" type="file" onChange={fileChange} name="file" />      
                <div className={classes.wrapper}>
                    <Button type="submit" 
                          variant="contained" 
                          color="secondary" 
                          size="large" 
                          startIcon={<CloudUpload />} 
                          disabled={props.id.length !== 8 || disabled} 
                          onClick={handleClick}>
                        Upload
                    </Button>
                </div>
            </form>
        </div>   
    )
}

서비스가 받아야 하는 파일 형식은 zip 파일이어야 한다.

따라서 HTML의 input 태그에서 받을 수 있는 파일 형식을 application/zip으로 지정했다.

우리 학교의 학번은 8자리니까, 최소한의 제약조건으로 8자리의 학번을 입력하지 않거나 파일이 없으면 업로드 버튼을 비활성화했다.

파일을 잘못 올렸을 때를 대비해 fileChange와 같은 핸들링 함수를 선언했고,

ButtonMaterial-UI의 컴포넌트를 임포트해 사용했다.



// file: 'FileTransfer.js'
    const fileChange = (e) => {
        setfile(e.target.files[0])
    };
}

이렇게.

파일을 여러 개 받지는 않으니까, 첫 번째 파일만 교체해주면 된다.

여러 개를 받고 수정하려면 저 배열을 반복문으로 수정해주면 될 듯 하다.



Upload


업로드 버튼이 만들어졌다.



뭔가 아쉬운데…

백엔드를 확인해보니 업로드도 잘 되는데, 뭔가 심심하다.

버튼을 누르고 아무 반응이 없으니 업로드가 잘 된 건지도 잘 모르겠다.



뭘 추가하면 좋을지 고민하면서 여러 웹 사이트를 탐색했는데, Progress를 나타내주면 좋을 것 같았다.

버튼을 눌러 전송하는 동안 Circular progress를 보여주면 진행 상황도 한 눈에 보이니까.🤔

Material-UI에서는 <CircularProgress> 컴포넌트도 제공하니 사용해보도록 하자!



// file: 'FileTransfer.js'
    return (
        <div>
            <form onSubmit={upload} className={classes.form}>
                <input accept="application/zip" type="file" onChange={fileChange} name="file" />      
                <div className={classes.wrapper}>
                    <Button type="submit" 
                          variant="contained" 
                          color="secondary" 
                          size="large" 
                          startIcon={<CloudUpload />} 
                          disabled={props.id.length !== 8 || disabled} 
                          onClick={handleClick}>
                        Upload
                    </Button>
                    {loading && <CircularProgress size={24} className={classes.buttonProgress} />} // NEW!
                </div>
            </form>
        </div>   
    )
}


Progress를 넣는 건 좋은데, 아무 이유 없이 계속 돌고있으면 예쁘지 않다.

loading이란 변수를 활용해 조건부 렌더링을 적용하고 버튼을 눌렀을 때부터 ~ 파일 전송이 끝날 때까지

컴포넌트를 렌더링하도록 하자.



// file: 'FileTransfer.js'

function FileUploadComponent (props) {
   const [file, setfile] = useState(null);
   const [disabled, setdisabled] = useState(true);
   const [loading, setloading] = useState(false);
   
   ...
   

loading 또한 hook을 사용해 값을 초기화 / 변경해줄 것이다.


// file: 'FileTransfer.js'
    const upload = (e) => {
        e.preventDefault();
        fileUpload(file)
            .then((response) => {   // New from
                setloading(false)
            })
            .catch((response) => {
                setloading(false);
            })                      // to here!
    };
}

fileUpload 함수는 axios.post 함수를 리턴하며, .then().catch() 함수를 사용해

각각 성공적으로 요청을 보냈을 때 / 에러가 발생했을 때 상황을 처리해줄 수 있다.

따라서 성공했든 아니든 Circular Progress를 멈춰주어야 하므로 setloading(false)를 사용해

더이상 렌더링되지 않게 했다.



마지막으로,


// file: 'FileTransfer.js'
    const handleClick = () => {
        if (!disabled) {
            setloading(true);
        }
    }

버튼을 눌렀을 때의 이벤트 핸들러 함수 또한 정의해주었다.

버튼이 disable 상태일 때는 Progress가 필요 없으니까 조건을 주었다.



Upload Progress

Progress를 보여주니 훨씬 직관적인 것 같다👍



🔥 TroubleShooting & Review

Issue:

  • axios 공식 문서를 잘 정독했다고 생각했지만 paramsheaders에서 은근히 많은 시간을 쏟았다. 문서를 꼼꼼히 보는 습관을 들이자…!
  • 조건부 렌더링을 처음 사용한 컴포넌트. 엄청 유용하다. 앞으로 많이 사용하게 될 것 같다
  • 이번에도 역시 Typescript에서 타입을 찾는데 고생해서 Javascript로 돌린 케이스다. 이후에 정확한 타입을 파악해 재작성해 봐야겠다.