본문 바로가기

개발일지/React

[노마드 코더] ReactJS 트위터 클론 코딩 - Uploading Images with S3

반응형

이 포스팅은 노마드 코더님의 트위터 클론코딩을 학습 후 작성하였습니다. (무료!!)

https://nomadcoders.co/nwitter

 

트위터 클론코딩 – 노마드 코더 Nomad Coders

React Firebase for Beginners

nomadcoders.co


 이번에는 트윗에 같이 나타날 이미지를 업로드해 볼 것이다. firebase의 Storage를 사용해도 좋지만, 정책변경으로 요금제를 업그레이드를 해야 사용할 수 있게 변경되어서 예전에 백엔드 프로젝트에서 사용했던 Amazon의 S3(Simple Storage Service)를 이용해 볼까 한다. 

 post-tweet-form.tsx에서 onSubmit() 함수 안의 addDoc() 함수가 document를 업로드한 이후에, 사용자가 file을 선택했는지 체크를 하고, 만약 file이 존재한다면 업로드를 해줄 것이다. 이미 S3의 버킷을 만들어 두었기 때문에 버킷에 관한 설정은 아래의 문서를 참조하자

https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/s3-example-photo-album.html

 

브라우저에서 Amazon S3에 사진 업로드 - AWS SDK for JavaScript

인증되지 않은 사용자의 액세스를 활성화하면 버킷과 해당 버킷의 모든 객체, 전 세계 모든 사람에게 쓰기 권한을 부여하게 됩니다. 이러한 보안 태세는 이 사례의 기본 목표에 초점을 맞추는

docs.aws.amazon.com

 우선 S3를 사용하기 위해 AWS SDK for JavaScript를 설치하자.

npm install aws-sdk

 최상위 폴더에 .env 파일을 생성하고, S3의 ACCESS_KEY와 SECRET_KEY를 등록해 준다. git에 노출되지 않도록 주의하며. gitignore에. env 파일을 등록해 주자.

// .env 
VITE_ACCESS_KEY = "발급받은 엑세스키"
VITE_SECRET_KEY = "발급받은 시크릿키"

  PostTweetFrorm() 함수 안에 S3기본 설정을 해준다.

import AWS from "aws-sdk";
 
const ACCESS_KEY = import.meta.env.VITE_ACCESS_KEY
const SECRET_KEY = import.meta.env.VITE_SECRET_KEY
const REGION = "ap-northeast-2";
const S3_BUKKET = "sulgibucket";
AWS.config.update({
    accessKeyId: ACCESS_KEY,
    secretAccessKey: SECRET_KEY,
});
const myBuket = new AWS.S3({
    params: { Buket: S3_BUKKET },
    region: REGION,
});

 액세스키와 시크릿 키를 임폴트 해주고, 지역과, 버킷 이름을 string으로 초기화시켰다. 그리고 AWS.config.update()로 액세스키와 시크릿 키를 등록해 주고, new AWS.S3()로 새로운 버킷을 생성해 주었다.

 하지만 일반 React 프로젝트에서는 문제가 없었으나, Vite기반 React 프로젝트에서는 S3를 불러오는 과정에서 "global is not defined"라는 오류가 생겼다. 두 가지 방법으로 해결이 가능했다.

https://dhwld5.tistory.com/223

 

Uncaught ReferenceError: global is not defined at node_modules/buffer/index.js

"global is not defined"  vite를 이용해 react 프로젝트를 하던 중 aws-sdk를 설치 후 AWS관련 패키지를 이용하려고 했을 때 만났던 오류이다. vite를 이용하지 않고 react프로젝트를 만들었을 시에는 오류가

dhwld5.tistory.com

 이제 file의 유무를 확인한 후 file이 존재한다면 s3 bukket에 업로드할 것이다. 업로드 후 image file의 url을 받아주고, 기존의 doc을 업데이트 해 줄것이다.  try구문을 보면 다음과 같다.

try{
    setLoading(true);
    const doc = await addDoc(collection(db, "tweets"), {
        tweet,
        createdAt: Date.now(),
        username: user.displayName || "Anonymous",
        userId: user.uid,
        photo : "",
    });

    if(file) {
        //S3 upload
        const params = {
            ACL : "public-read",
            Body : file,
            Bucket : S3_BUKKET,
            ContentType: file.type,
            Key : "upload/" + file.name,
        };
        const url = await myBuket.upload(params, (error:any)=>{
            console.log(error);
        }).promise()
        await updateDoc(doc, {
            photo: url.Location
        })
    }
}catch(e) {
    console.log(e);
}finally {
    setLoading(false);
}

 데이터 베이스에도 잘 들어온 것을 확인할 수 있다.

 

 간단한 이미지만 업로드 할것이기 때문에 이미지 업로드 시에 파일용량의 제한을 주는 게 좋다. 

const onFileChange = (e: React.ChangeEvent<HTMLInputElement>)=> {
    const{files} = e.target;
    if(files && files.length === 1) {
        if(files[0].size < 1*1024*1024){
            setFile(files[0]);
        }else {
            alert("image file size should be less than 1Mb");
        }
    }
}

onFileChange함수에서 file의 size를 체크해 1mb이상의 파일만 등록을 하고, 파일이 크다면 alret을 통해 알람을 주었다.

 

post-tweet-form 전체 코드

import { addDoc, collection, updateDoc } from "firebase/firestore";
import React, { useState } from "react";
import styled from "styled-components"
import { auth, db } from "../firebase";
import AWS from "aws-sdk";

const Form = styled.form`
    display: flex;
    flex-direction: column;
    gap: 10px;
`;

const TextArea = styled.textarea`
    border: 2px solid white;
    padding: 20px;
    border-radius: 20px;
    font-size: 16px;
    color: white;
    background-color: black;
    width: 100%;
    resize: none;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    &::placeholder {
        font-size: 16px;
    }
    &:focus {
        outline: none;
        border-color: #1d9bf0;
    }
`;

const AttachFileButton = styled.label`
    padding: 10px 0px;
    color: #1d9bf0;
    text-align: center;
    border-radius: 20px;
    border: 1px solid #1d9bf0;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
`;

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

const SubmitBtn = styled.input`
    background-color: #1d9bf0;
    color: white;
    border: none;
    padding: 10px 0px;
    border-radius: 20px;
    font-size: 16px;
    cursor: pointer;
    &:hover,
    &:active {
        opacity: 0.8;
    }
`;

export default function PostTweetFrorm() {
    const ACCESS_KEY = import.meta.env.VITE_ACCESS_KEY
    const SECRET_KEY = import.meta.env.VITE_SECRET_KEY
    const REGION = "ap-northeast-2";
    const S3_BUKKET = "sulgibucket";
    AWS.config.update({
        accessKeyId: ACCESS_KEY,
        secretAccessKey: SECRET_KEY,
    });
    const myBuket = new AWS.S3({
        params: { Buket: S3_BUKKET },
        region: REGION,
    });
    //-------------------------------------------
    const [isLoading, setLoading] = useState(false);
    const [tweet, setTweet] = useState("");
    const [file, setFile] = useState<File|null>(null);
    const onChange = (e:React.ChangeEvent<HTMLTextAreaElement>) => {
        setTweet(e.target.value);
    }
    const onFileChange = (e: React.ChangeEvent<HTMLInputElement>)=> {
        const{files} = e.target;
        if(files && files.length === 1) {
            if(files[0].size < 1*1024*1024){
                setFile(files[0]);
                console.log("file on");
            }else {
                alert("image file size should be less than 1Mb");
            }
        }
    }
    const onSubmit = async(e:React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        const user = auth.currentUser
        if(!user || isLoading || tweet === "" || tweet.length > 180) return;
        try{
            setLoading(true);
            const doc = await addDoc(collection(db, "tweets"), {
                tweet,
                createdAt: Date.now(),
                username: user.displayName || "Anonymous",
                userId: user.uid,
                photo : "",
            });

            if(file) {
                //S3 upload
                const params = {
                    ACL : "public-read",
                    Body : file,
                    Bucket : S3_BUKKET,
                    ContentType: file.type,
                    Key : "upload/" + file.name,
                };
                const url = await myBuket.upload(params, (error:any)=>{
                    console.log(error);
                }).promise()
                await updateDoc(doc, {
                    photo: url.Location
                })
            }
        }catch(e) {
            console.log(e);
        }finally {
            setLoading(false);
        }
    }
    return (
    <Form onSubmit={onSubmit}>
        <TextArea required rows={5} maxLength={180} onChange={onChange} value={tweet} placeholder="What is happening?"/>
        <AttachFileButton htmlFor="file">{file ? "Photo added ✅" : "Add photo"}</AttachFileButton>
        <AttachFileInput onChange={onFileChange} type="file" id="file" accept="image/*" />
        <SubmitBtn type="submit" value={isLoading ? "Posting..." : "Post Tweet"}/>
    </Form>
    )
}

 

 

이전 글 :

[노마드 코더] ReactJS 트위터 클론 코딩 - Router
[노마드 코더] ReactJS 트위터 클론 코딩 - Authentication
[노마드 코더] ReactJS 트위터 클론 코딩 - Github Login
[노마드 코더] ReactJS 트위터 클론 코딩 - Tweeting

 

다음 글 :

[노마드 코더] ReactJS 트위터 클론 코딩 - Fetching Timeline

[노마드 코더] ReactJS 트위터 클론 코딩 - Realtime Tweets & Delete

[노마드 코더] ReactJS 트위터 클론 코딩 - Edit tweet(code challenge)

[노마드 코더] ReactJS 트위터 클론 코딩 - Pfofile page

반응형