[React] FileReader, FileAsDataUrl 를 이용해 프로필 사진 입력받아 미리 보여주고, 서버에 전송하기

안지수·2023년 9월 11일
0


로컬에 있는 이미지 서버에 전송 전에, 사용자에게 미리 보여주기를 구현하고자 하였다. 여기서 프로필 이미지를 클릭하면, 로컬의 이미지를 선택해 미리 보여줄 수 있게하고, 하단의 '다음'버튼을 눌렀을 때, 그 이미지가 서버에게 전송될 수 있게끔 하고 싶었다. 프로필 사진 업로드는 처음 해 봐서, 이것저것 시도해보았다.

😀 프로필 사진 입력받는 방법

input type="file" 통해서 입력받기


: 위와 같이 input을 통해서 입력받을 경우, 내가 선택한 파일 명만 확인할 수 있을 뿐, 미리보기 단계를 거치지 못한다. 그래서 검색을 하다보니, 프로필 사진을 입력받아서 미리보기로 보여주기 위한 방법은 아래와 같이 2가지가 있음을 알 수 있었다.

1. FileReader, FileAsDataUrl

2. createObjectUrl

--> createObjectURL을 사용하면 메모리 관리 측면에서 좀 더 효율적이며, 대용량 파일을 다룰 때 유용합니다. FileReader와 FileAsDataURL은 파일 내용을 문자열로 변환하여 사용하므로, 큰 파일을 처리할 때는 성능 이슈가 발생할 수 있다.

👌 나는 FileReader, FileAsDataUrl를 이용한 첫 번째 방법을 선택했다.

😀 코드 분석

: 코드 한줄, 한줄 분석을 해보겠다.

❤️ 코드 설명 전 알아야할 것

  • image_file: 진짜 파일 형태로, 서버에 보낼 수 있어야 함
  • preview_URL: 단순히 이미지를 사용자한테 잠깐 보여주기만 하면 됨
    : 따라서 파일과 url을 모두 저장하기 위해서, state를 아래와 같이 정의하였다.

    -> img의 초기 state는 객체로서, 2개의 속성을 가진다. 즉, 파일이 업데이트되면, 해당 이미지의 파일 정보와 url 정보를 모두 저장할 수 있게한다.

❤️ jsx코드

  • 'input type="file"' 를 통해 이미지를 입력받고 있다. accept 속성을 통해, 업로드 할 수 있는 파일의 형태를 지정해주고 있다. (accept 속성은 type이 file인 경우에 사용되는 속성)
    "image/*"은 2 부분으로 나눌 수 있다.

-> image/: 파일 종류를 정의. 이미지 파일 업로드
-> *: 파일의 형식 지정해줌. 별 표의 경우 어떤 이미지 파일이든 업로드 가능하다는 뜻이다. (png, jpeg 등)

  • style을 display:none으로 지정하여, 입력란이 보이지 않게 처리하였다. 만약 그 속성이 없다면, 위와 같이 파일을 업로드 할 수 있는 입력란이 보일 것이다.

  • none으로 지정한 대신, id를 지정하여 input을 받을 곳을 아랫줄의 Label로 연결시켜주었다. 즉, Label 태그 안에 있는 사진을 클릭하였을 때, 이미지 사진을 받을 수 있도록 연결시켜준 것이다.
    htmlFor 속성은 label 태그에서 쓰이는 것으로, label과 input을 연결해줄 때 사용하는 속성이다.

  • label 태그 안에는 사진을 입력받는 태그들이다. 피그마에 있던 대로, 동그라미 이미지를 가져와주었고, Camera 라는 사진을 가져와 그 위에 위치 시켜주었다.
    'src={image.preview_URL || profileCircle}' 이 부분은 왼쪽 피연산자 'image.preview_URL'가 false이면 || 오른쪽의 피연산자 값을 반환한다. 즉, 사용자에게 프로필 사진을 입력받은 것이 없다면, 기본 이미지인 'profileCircle'를 보여주게 하였다.

--> 결국엔, 그 프로필 사진을 입력받기 위해서는, 기본 이미지를 클릭하여, 입력받을 수 있게 된다. 사용자가 프로필 사진을 등록하면, input값이 변경되면, onChange 이벤트 핸들러가 실행되어 그에 연결된 'handleImageChange'함수가 실행된다.

❤️ handleImageChange 함수


: 파일을 선택을 하면, 이벤트 핸들러에서 이 함수의 호출로 인해 파일이 처리되고, 업데이트가 이루어진다. 코드를 한줄 한줄 살펴보자.

1. const selectedFile = e.target.files[0];

  • e는 이벤트 객체로 input에 값을 입력한 이벤트 정보를 담고있음
  • e.target은 이벤트가 발생한 DOM 요소, 즉 파일 업로드 input 요소
  • e.target.files[0]은 사용자가 선택한 파일 중 첫 번째 파일

2. const reader = new FileReader();

: FileReader 객체를 생성하고, 이 객체는 파일을 읽는 데 사용

3. reader.onload

: FileReader 객체의 onload 이벤트 핸들러를 설정합니다. 파일 읽기 작업이 완료되면 이 핸들러가 호출

4. reader.readAsDataURL(selectedFile);

: FileReader 객체를 사용하여 선택한 파일을 읽어옵니다. 이 작업은 비동기적으로 수행
파일 읽기 작업이 완료되면, reader.onload 핸들러가 호출되고, 파일의 데이터 URL이 e.target.result에 포함

5. setImage 함수 호출

: 파일 읽기가 완료되면, setImage 함수를 호출하여 React 컴포넌트의 image 상태를 업데이트

  • image_file 속성에는 사용자가 선택한 파일(selectedFile)이 저장
  • preview_URL 속성에는 파일의 데이터 URL(e.target.result)이 저장

2. CSS 적용

- 이미지 동그라미 안에 꽉 차게


: width, height, border-radius, overflow를 활용해 사진을 업로드하게끔
-> 사진에서는 이미지가 동그라미 안에 꽉 차게 하고, object-fit 속성을 활용하여 이미지가 꽉 차게 하였다.

- 프로필 사진 변경 후, 가운데 정렬


-> 왼쪽으로 치우쳐져 있었다.

-> 그 프로필들을 전체적으로 감싸는 label 태그에 위와 같이 설정하였더니, 해결이 되었다. 정렬이 원하는 대로 되지 않는 경우에는, 그것들을 감싸는 부모 태그의 정렬들을 살펴보자.

  • 카메라 모양 계속 뜨게

    : 사진 전체를 감싸는 태그에 position: relative를 설정하고, camera 사진에는 position: absolute를 설정하여, 이미지가 계속 뜨게하였고, 이를 통해 사진 선택 이후에도 계속 사진을 재변경 할 수 있게 의도하였다.

😀 백엔드에게 코디네이터 정보 전송 과정

: 코디네이터는 키, 닉네임, 나이, 몸무게, sns 링크 등과 같은 정보 이외에도, 등록한 프로필 사진과 함께 넘겨주어야 했다. 따라서 그 원리는 동작 과정은 아래와 같다.

❤️ 클라이언트에서 코디네이터 정보 벡으로 전송하는 과정

'다음' 버튼을 누르면 aws s3에 이미지가 업로드되어, 응답으로 링크를 돌려주고, 그 이외의 정보들과 함께 서버에 post 요청을 보내게 된다.
-> 그렇다면, '다음'버튼을 눌렀을 때 s3에 업로드 해주면 늦지 않냐고 물어볼 수 있다.
-> 하지만, 사진 업로드 및 링크 반환은 비동기적으로 진행되어 링크를 응답으로 받은 이후에 다른 정보들과 함께 post 요청을 보내게 되는 것이다.

-> npm install aws-sdk 사진 업로드를 위해, 작업에 앞서 라이브러리를 설치를 해준다.

😀 코드 분석

    const handleImageUpload = () => {
        AWS.config.update({
            region: "ap-northeast-2",
            accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY_ID,
            secretAccessKey: process.env.REACT_APP_AWS_SECRET_ACCESS_KEY,
        });

        try {
            const file = image.image_file;

            const upload = new AWS.S3.ManagedUpload({
                params: {
                    Bucket: "seumu-s3-bucket", // 버킷 이름
                    Key: "test.jpeg", // 파일 이름 (버킷 안에서 저장될 파일 이름)
                    Body: file, // 파일 객체
                },
            });

            const promise = upload.promise();   // 반환값을 받음

            promise.then((data) => {
                async function fetchData() {
                    try {
                        const res = await axios.post('http://localhost:8080/coordinator/profile',
                            {
                                coordinator_id: 1,
                                nickname: nickname,
                                sns_url: sns_url,
                                image_url: data.Location,
                                content: content,
                                gender: male === true ? 'MALE' : 'FEMALE',
                                height: height,
                                weight: weight,
                                total_like: 0,
                                request_count: 0
                            }
                        );
                        console.log(res);
                    } catch (error) {
                        console.error(error);
                    }
                }

                fetchData();
            });
        } catch (e) {
            console.log(e);
        }
    };

: 전체 코드는 위와 같다. 즉, 다음버튼을 눌렀을 때 실행되는 함수로, s3에 이미지 업로드하고, 그 응답으로 받은 이미지 링크 정보와 다른 정보들을 벡엔드를 전송해주는 내용이 모두 작성되어있다. 또, 그 코디네이터가 작성한 정보들을 전달하기 위해 상태로 관리해주었고, 아래와 같이 상태를 추가해주었다. 코드를 axios 통신, s3 이미지 업로드, 이벤트 발생 코드로 나누어 살펴보자.

코드를 이해하기 앞서, 동기/비동기의 개념을 이해해야 한다. 뒤에서 작성할 것이지만, 이 부분을 간과하여, 에러를 해결하는데, 많은 시간을 보냈기 때문이다. 원래 처음 코드는 위와 같지 않았고, 최종 코드이다. 에러가 났던 처음 코드는 아래에 첨부하였다. 일단, 최종 코드를 한줄 씩 자세히 설명해보겠다.
동기/비동기

❤️ 이미지 s3 업로드와 서버 전송 코드

AWS SDK의 구성을 업데이트


-> 키 값은 노출시키면 안되므로, process.env에 환경변수로 설정해서 가져와 주었다. 이것과 관련해서도 에러가 발생하였는데, 이 또한 아래 에러 해결 과정에서 작성해보도록 하겠다.

업로드할 파일 가져오고, 객체 생성


-> 위에서 사용자에게 업로드하라고 입력받은 파일을 file 변수에 저장해준다. try는 예외가 발생할 수 있는 곳을 감싸는 것이다. 따라서 그 안에서 예외가 발생하면 그 밖의 catch 블록으로 제어가 이동한다.
-> s3에 파일을 업로드하기 위한 ManagedUpload객체를 생성한다. 업로드할 파일의 정보, 어떤 이름으로 업로드 할지, 버킷 이름 등을 포함하여 객체를 생성하고 그 이름을 upload라고 한다.

업로드


-> 위에서 생성한 객체로 비동기적인 업로드 작업을 시작한다. 따라서 결과가 반환되면 promise 변수에 반환값을 받는다. 보통 data라는 변수에 반환값이 변환된다.

성공적으로 업로드 시, 모든 정보들과 함께 서버로 전송


-> 업로드 작업이 성공적으로 완료되면, then 메서드가 호출된다. data에는 업로드된 이미지의 정보가 포함된다. 업로드가 완료되면, 서버에 정보를 전송하기 위한 fetchData함수가 실행된다. 이러한 axios 통신은 보통 비동기적으로 일어난다. 왜냐하면 서버로부터 응답을 받을 때까지 기다렸다가 결괏값을 반환해줘야 하기 때문이다. s3에 업로드한 이미지는
'image_url: data.Location'와 같이, 업로드한 이미지의 주소를 반환해주고 있다. 또, try, catch 문을 통하여 그 안에서 예외 발생을 처리해주고 있다. /coordinator/profile 엔드포인트에 POST 요청을 보내고, 이미지 URL과 함께 다른 데이터도 전송하고 있다.
-> await 키워드는 axios.post 함수의 비동기 작업이 완료될 때까지 대기
-> 이미지 업로드가 완료되면, fetchData()함수 호출을 먼저 수행하는 것이다. 서버로 전송을 보내기 위한 시작이라고 할 수 있다.

⭕ 전체적인 흐름 나의 언어로 정리

: 코디네이터의 정보를 입력받는 페이지에서 정보 모두 입력 후 하단의 다음 버튼을 누르면 위의 함수가 실행되는 것이다. 일단, try를 통해 이미지를 업로드하기 위한 준비를 하고, 서버에 그 결괏값을 전송하는 과정인 것이다. 비동기적으로 처리하여 이미지 업로드 결괏값을 기다린다. (객체 생성) 그리고, 성공적으로 이미지가 업로드 되면, then 함수를 통해서, 서버로 전송하기 위한 함수를 호출하고, 호출되면 비동기적으로 그 함수가 실행되어, 업로드한 이미지 위치가 지정되고, 서버로 데이터들이 전송된다. 그 과정에서 예외를 처리해주어, 예외 발생 시, catch문으로 이동하게끔 하였다.
즉, promise를 통해 이미지 업로드를 비동기적으로 처리하고 있고, async/await를 통하여 서버 전송을 비동기적으로 처리하고 있다. (이미지 업로드 성공 시) 또, try/catch문을 통하여 위 과정들의 예외를 처리해주고 있다.

❤️ 상태와 이벤트 발생 코드 수정

  1. CoInfo.js 정보 수정 페이지

  2. GetInfo.js 정보 입력 페이지

    -> 부모의 그 props는 직접 그 이벤트가 수행되는 자식 컴포넌트까지도 전달되어져야 한다는 것!!!

  3. 이벤트 핸들러 함수들

    -> 각 상태들이 변화하면, (여기선 input창에 입력받는 것이므로) onChange 이벤트가 발생하면, 이렇게 useState 훅을 통해 재렌더링되어, 상태값들을 업데이트 시켜주고 있다. 마지막엔, 이 모든 값들을 서버에 전송해주어야 하는 것이다.

❤️ 그런데?!



-> 상태를 수정하고 나니, textarea 작성하는 부분에서 글자 수 상태 관리 부분에서 결과가 제대로 나오지 않았다. 작성하는 글자수에 비해 하나씩 적게 나오는 것이다.

  • 원인: setContent를 통해 입력한 내용을 content 상태에 반영되기 전에 setInputCount가 실행되어 하나 적게 출력이 되는 것이다.
    -> 즉, content 상태가 업데이트가 되기 전에 content를 사용하여 발생한 것이다. 즉, 동기/비동기 관련한 문제점이라고 할 수 있다.
    -> 동기적으로 수행되어서, 문제가 발생한 것이다. content가 업데이트될 때까지 기다리지 않고, 그 변수 값을 이용해서 발생한 문제이다.
  • 해결 방안: 업데이트가 content 상태에 업데이트 될 때까지 기다릴 것 없이, 실시간으로 업데이트되는 e.target.value의 length를 검사하여 이 문제를 해결하였다.

😀 벡엔드와의 통신 과정에서 발생한 오류와 해결 방법

사진의 url을 저장하고, 그 url을 포함한 코디네이터 정보를 서버에게 정보를 전송하는 과정에서 엄청난 시간을 쏟았다. 계속해서 에러가 발생해서, 이를 해결하는 과정에서 많은 좌절과..노력이 있었다고 한다!!!!

1. 도커 실행 에러

  • 원인: workbench랑 port가 겹쳤다. 도커에서 접근하려는 port가 이미 차지되어있던 것이다.
  • 해결 방안: workbench port를 사용 중지를 해주고, workbench 를 삭제해주었더니, 도커 오류는 해결되었다.

2. undefined 에러

  • 원인: 초기값을 지정을 해주고 단지 useState()라고만 써주어 문제가 발생한 것이다.
  • 해결 방안: 초기값을 지정해줌으로서 쉽게 이 에러는 해결하였다.

3. cors 에러 💢💢💢💢

: 이 에러 때문에...진짜 많은 시간을 보냈다. 근본적인 원인은 1번 문제 때문이었다...자세한 내용과 해결 방법은 아래 블로그 맨 아래 해결 방법 부분에 정리를 해 놓았다.
cors 에러 해결

: 다른 팀원들의 컴퓨터에서는 모두 다 정상적으로 작동하는데, 내 컴퓨터에서만 cors오류가 나는 것이다. 확장 프로그램도 설치해보고, 요청 origin과 백엔드에서도 헤더에 allow 해준 origin도 모두 확인해 본 겨로가 유효하였다...그럼 대체 왜?

  • 원인: 3306 포트 차지
  • 해결 방안: axios 요청에서 '127.0.0.1:8080' 대신에 localhost:8080으로 바꿔주고, 3306 포트 (workbench) 사용을 중지 시켜줌으로서, 해결하였다.

4. 사진 전송 url 생성 실패

: cors 문제를 해결하였지만, 이번엔.... s3에 업로드한 이미지의 url을 받아오지 못하는 문제가 발생하였다. 업로드 조차 제대로 이루어지지 못했을 수도 있다. 따라서 아래 2가지 해결 방식을 적용시켜 보았다.

🔶 캐시 삭제

: 앞서 언급했던 사진 업로드를 위한 aws의 키 값이 노출되면 안되는데, 그 것이 노출된 것이 문제인가라고 생각하였다. 따라서, gitignore이 제대로 동작하지 않는 것으로 보고, 캐시를 삭제해보았다. 아래와 같은 명령어로 말이다. 하지만 여전히 github에 그 .enc.development 파일이 노출되는 것이다.

https://jojoldu.tistory.com/307
(이 사이트를 참고 함)

🔶 gitignore 파일 수정


: 그래서 이번엔 gitignore에 문제가 있다고 생각하여, 검색한 결과 '.env.development.local' 또한 무시 목록으로 추가해줬어야 하는 것이다. 이를 통해 키 값을 노출시키는 문제는 해결이 되었다.
하지만, img_url이 제대로 생성되지 않는 문제는 여전히 해결되지 않는 것이다. 이는 코드가 문제라는 생각이 들었고, 코드를 살펴보았다.

🔶 코드 수정

<원래 코드>

    const handleImageUpload = () => {
        console.log("제대로 된다.");

        async function fetchImage() {
            AWS.config.update({
                region: "ap-northeast-2",
                accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY_ID,
                secretAccessKey: process.env.REACT_APP_AWS_SECRET_ACCESS_KEY,
            });
            try {
                const handleFileInput = async () => {
                    const file = image.image_file;

                    const upload = new AWS.S3.ManagedUpload({
                        params: {
                            Bucket: "seumu-s3-bucket", // 버킷 이름
                            Key: "test.jpeg", // 파일 이름 (버킷 안에서 저장될 파일 이름)
                            Body: file, // 파일 객체
                        },
                    });

                    const promise = upload.promise();   // 반환값을 받음

                    promise.then((data) => {
         
                        setImage_url(data.Location);
                    });
                }
            } catch (e) {
                console.log(e);
            }
        }

        async function fetchData() {
            try {
                const res = await axios.post('http://127.0.0.1:8080/coordinator/profile', 
                {
                    coordinator_id: 1,
                    nickname: nickname,
                    sns_url: sns_url,
                    image_url: image_url,
                    content: content,
                    gender: male === true ? 'MALE' : 'FEMALE',
                    height: height,
                    weight: weight,
                    total_like: 0,
                    request_count: 0
                },
                {
                    headers: { 'Content-Type': 'application/json'},
                }
                );

                console.log(res);
            } catch (error) {
                console.error(error);
            }
        }

        fetchImage();
        fetchData();
    };

원래 코드는 위와 같다. 위의 코드에서 위 쪽에서 최종 코드라고 언급한 코드로 수정했더니 사진 전송 문제를 완벽히 해결할 수 있었다.

  • 원인: 동기/ 비동기 문제/ 결괏값이 반환되기 이전에 그 다음 작업이 수행되어져버림/ 즉, promise 값이 참 일 때, 이미지가 정상적으로 업로드가 되었을 때, 서버에 전송하게 끔 해야하는데, 여기서는 그냥 '다음' 버튼이 눌렸을 때, 이미지 업로드 전에 바로 서버 전송 함수를 실행해버리는 것이다. 그러니까 당연히 img_url이 빌 수 밖에...
  • 해결 방법: axios 통신 부분 코드를 promise then안으로 이동하였다. 따라서 이미지 업로드 성공 시, 서버와 통신하게끔 처리하였다. 비동기가 적절히 수행되어져, 모든 정보들이 적절히 전송되었다.
    업로드중..
    -> 아래와 같이 img_url이 정상적으로 반환되어졌고, 다른 정보들과 함께 서버로 정보가 잘 전송이 되었음을 알 수 있다.

--> 동기/ 비동기 개념을 공부하고 나니, 이 에러의 원인을 확실히 파악할 수 있었던 것 같다. 그래서 코드를 한 줄 한 줄 아는 것이 중요하다고 하는 것 같다. try/catch, then, asyn, promise 의 쓰이는 방법과 코드 수행 방법, 개념을 잘 알지 못했다. 그래서 해결도 막막하였다. 그래서 코드를 한 줄 한 줄 보자는 생각을 하였고, 그렇게 동기/ 비동기를 공부하게 되었다. 그러면서, 위의 에러도 손쉽게 해결할 수 있었던 것 같다. 이렇게 에러 하나를 해결해 나가는 과정에서 배우는 게 정말 많은 것 같다.

---> 이 사진 전송을 구현하고, 백과 데이터를 주고 받는 과정에서 발생한 오류가 정말 많기 때문에..... 웬만한 오류는 무섭지 않을 것 같다. 그 어렵다는 cors 문제도 해결했기 때문에!!!!!!!

profile
지수의 취준, 개발일기

0개의 댓글

관련 채용 정보