본문 바로가기
Firebase/Nomadcoder Twitter

#4 Tweeting

by aeyong-dev 2024. 1. 19.

로그인을 성공했으니 이제는 트윗을 게시할 차례다. 

우리는 아래 두 단계를 거쳐 이를 구현 할 것이다.

  1. DB에 트윗 저장
  2. Storage에 첨부된 이미지 저장

DB에 트윗 저장하기

Firebase의 database는 none-sql database를 사용한다. 

이는 collection과 document로 이루어진다.

collection은 폴더, document는 파일과 비슷한 개념이라고 생각해도 좋다. 

예를 들어, Twitter 프로젝트에서는 tweets/${tweet's ID}/replies/${tweet's ID}.... 처럼 사용할 수 있다. 

tweets와 replies는 collection, tweet's ID는 document 이다. 

 

포스팅을 위한 form을 작성하면서 새로 배운 속성들이 있다. 

  • input 태그에서 accept는 확장자를 제한할 수 있다. 
    • ex) <input accept="image/*">
      • 이미지 파일만 수락하고 확장자는 상관이 없다. 
    • ex) <input accept="image/png">
      • 확장자가 png인 이미지 파일만 수락한다. 
  • label 태그의 htmlfor
    • input태그의 id와 htmlfor가 같은 label을 연결한다. 
    • <label htmlfor="file"> ... <input id="file"> 에서 두 태그는 연결되어있다. 
  • input 태그의 type="TextArea", 크기 설정 관련
    • TextArea는 기본적으로 크기 조정 도구를 내장하고 있다.
    • 크기 조정을 불가능하게 하려면? 
      • css>> resize: none;

포스팅을 위한 form이 준비되었다면 본격적으로 firebase 연동을 할 차례다. 

인증 기능과 마찬가지로 firebase console에서 activate 해줘야 한다. 

database를 사용하기 위해서는 cloud firestore를 activate 하면 된다. 

activate 이후에는 storage server를 생성하면 firebase console의 설정은 완료된다. 

 

firebase.js에서 상품을 활성화해보자. 

간단하다. export const dv = getFirestore(app); 한 줄로 활성화가 가능하다!

database에 접근할 때는 이 storage를 import 해서 사용하면 된다..

 

그럼 이제 데이터를 storage에 저장해 볼 차례다. 

만든 form에 onclick 함수를 연결하여 전송하면 될 것이다. 

어떻게 데이터를 전송할까? 

 

먼저 어떤 사용자에게 저장할지를 정해야한다. 

당연히 현재 로그인 한 현재 사용자의 데이터에 저장해야한다. 

firebase.js에서 auth를 import해서 auth.currentUser를 변수에 저장한다. 

 

addDoc() 으로 document를 추가할 수 있다. 

parameter로 저장할 collection과 저장할 데이터를 넘겨주면 된다. 

 

collection은 collection() 메소드로 가져올 수 있다. 

firebase.js에서 db를 가져오고(import 하고), 이름이 "tweets"인 collection을 가져오라는 뜻이다. 

 

addDoc()에 두번째 parameter로 넘겨줄 데이터는 중괄호를 사용해 JSON의 형태로 작성하면 된다. 

아래는 예시코드이다. 

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

 

여기서 생성한 document는 doc으로 참조할 수 있다. 

 

이렇게 해서 데이터 추가는 완료되었다. 

비슷한 방식으로, 업데이트는 updateDoc, 삭제는 deleteDoc으로 간단히 구현할 수 있다. 

 

Storage에 첨부된 이미지 저장

데이터를 storage에 저장했으니 이제는 첨부파일, 즉 이미지를 업로드 할 차례다. 

이미지를 업로드 하기 위해서는 firebase storage를 활성화 해줘야 한다. 

다른 기능들과 마찬가지로 firebase console - build - storage 탭에 들어가 활성화 할 수 있다. 

이쯤되면 다음 차례는 우리 프로젝트에서 활성화시켜야 한다는 것을 알 것이다. 

export const storage = getStorage(app) 으로 활성화 한 후 사용하자. 

 

Tweet upload button의 onclick으로 사진파일을 첨부하면 될 것이다. 

파일이 없다면, 즉 이미지를 첨부하지 않았으면 필요하지 않은 로직을 작성하는 것이니 if(file) 안에 작성하도록 하자. 

먼저 파일에 대한 위치 참조를 얻어야 한다. 

위치 참조는 ref()로 쉽게 얻을 수 있다. 

매개변수로는 (firebase.js에서 선언한) storage, storage 내부 경로(`tweets/{user.id}/{doc.id}`)를 전달한다. 

변수 locationRef에 할당해주자. 

이 위치 참조에다가 우리의 파일을 저장한다. 

uploadBytes()로 파일을 업로드하는데, 위치 참조와 파일을 매개변수로 넘겨준다. 

 

파일 업로드는 성공적이다. 하지만 해야할 일이 하나 더 있다. 

'이 트윗에 어떤 사진을 올렸는지'를 알 수 있어야 한다. 

트윗에 파일 링크를 첨부해주면 된다. 

uploadBytes()는 업로드 결과에 대한 Promise를 return한다. result 변수에 저장해주자. 

getDownloadURL()로 파일의 링크를 얻을 수 있다. 매개변수로 result.ref를 넣는다.

마지막으로 updateDoc()으로 photo: url 데이터를 추가하면 비로소 파일 업로드가 완료된다. 

 


Twitter 클론코딩을 위해 인증, 트윗 CRUD를 구현했다. 

Timeline을 업데이트하는 기능을 완성해보도록 하자. 

Timeline에 query문을 넣고 실시간으로 업데이트 되도록 할 것이다. 

 

Timeline.js를 작성한 후 Home.js에서 사용한다. 

<PostTweetForm />의 아래에 <Timeline />을 사용한다. 

스크롤을 통해 Timeline을 보고싶으니 CSS 속성 overflow-y: scroll을 준다. 

 

overflow-x(overflow-y)는 주어진 공간에서 내용이 넘칠 때 어떻게 할 것인지 정해준다. 

x, y는 x축(가로)기준인지, y축(세로)기준인지를 의미한다. 

  • visible: 내용물이 넘친다. 
  • hidden: 넘치는 내용들은 잘린다. 
  • scroll: 넘치는 내용들은 잘리되 스크롤을 할 수 있다. 
  • auto: 넘치는 내용들은 잘리되 필요할 경우에만 스크롤 할 수 있다. 

Firebase에서의 Query

query() 함수로 query문을 작성할 수 있다.. 

const tweetsQuery = query(
        collection(db, "tweets"),
        orderBy("createdAt", "desc"),
        limit(25)
      );

 

db에서 "tweets" collection을 가져와, "createdAt(생성일)" 기준으로 내림차순, 한번에 25개만 가져오도록(limit(25)) 쿼리문을 작성한다. 

getDocs(tweetsQuery)로 데이터를 가져오고, 이를 배열로 저장하면 완료된다. 

 

const snapshot = await getDocs(tweetsQuery);
      const tweets = snapshot.docs.map((doc) => {
        const { tweet, createdAt, userId, userName, photo } = doc.data();
        return {
          id: doc.id,
          tweet,
          createdAt,
          userId,
          userName,
          photo,
        };
      });

tweets 배열에 저장한다. 

snapshot에 데이터가 있으므로, map()을 통해 하나씩 저장한다. 

 

Realtime Query

database와 query 사이에 실시간 연결을 만들어주자. 

database에서 변화가 감지되면 query에 알림을 주고 자동으로 업데이트하게 한다. 

이 연결을 만들기 위해서는 onSnapshot() 함수를 사용하면 된다. 

query문과 함수를 넣어주는데, query는 위에서 사용한 tweetsQuery를 사용하면 된다. 

unsubscribe = await onSnapshot(tweetsQuery, (snapshot) => {
        const tweets = snapshot.docs.map((doc) => {
          const { tweet, createdAt, userId, userName, photo } = doc.data();
          return {
            id: doc.id,
            tweet,
            createdAt,
            userId,
            userName,
            photo,
          };
        });
        setTweets(tweets);
      });

함수는 구조분해할당으로 변경된 배열을 return한다. 

 

Deleting tweet

tweet 데이터 삭제는 deleteDoc()으로 간단히 구현할 수 있다. 

그럼에도 따로 소제목으로 분류한 것은 파일 때문이다. 

tweet 데이터를 삭제한다고 끝나는 것이 아니라 연결된 사진 파일도 삭제해야 한다. 

파일 삭제는 deleteObject() 함수를 사용한다. 

매개변수로 사진 파일에 대한 참조를 넘겨주면 된다. 

이 참조는 ref(storage, 파일 경로)로 간단히 구현 가능하다. 

 

Code challenge: tweet's edit

code challenge로 수정 버튼을 구현해야 했다.

const Home = () => {
  return (
    <Wrapper>
      <PostTweetForm />
      <Timeline />
    </Wrapper>
  );
};

트윗 작성 form과 timeline이 형제 태그로 존재하여 수정 form을 따로 작성해야했다. 

수정 중일 때는 tweet 내용 대신 수정 form을 보여주도록 tweet 컴포넌트를 수정했다. 

{isEditing ? (
          <EditTweetForm
            tweet={tweet}
            photo={photo}
            userId={userId}
            id={id}
            endEdit={endEdit}
          />
        ) : (
          <>
            <Payload>{tweet}</Payload>
            {photo ? (
              <Column>
                <Photo src={photo} />
              </Column>
            ) : null}
          </>
        )}

수정중인지를 나타내는 상태인 isEditing의 기본값은 당연히 false이다. 

true일 경우에는 EditTweetForm을 보여준다. 

const EditTweetForm = ({ tweet, photo, id, endEdit, userId }) => {
  const [text, setText] = useState(tweet);
  const [file, setFile] = useState(photo);

  const onTextChange = (e) => {
    setText(e.target.value);
  };
  const onFileChange = (e) => {
    setFile(e.target.files[0]);
  };
  const onSubmit = async () => {
    try {
      const tweetRef = doc(db, "tweets", id);
      if (!file) {
          await updateDoc(tweetRef, {
              tweet: text,
            });
      } else {
        const storageRef = ref(storage, `tweets/${userId}/${id}`)
        const result = await uploadBytes(storageRef, file);
        const url = await getDownloadURL(result.ref);
        await updateDoc(tweetRef, {
          tweet: text,
          photo: url,
        });
      }
    } catch (e) {
      console.error(e);
    } finally {
      endEdit();
    }
  };
  const onCancel = (e) => {
    e.preventDefault();
    endEdit();
  };
  return (
    <Wrapper>
      <TextInput onChange={onTextChange} value={text}></TextInput>
      <AttachFileButton htmlFor="edit">
        {file ? "Photo changed" : "Change photo"}
      </AttachFileButton>
      <AttachFileInput
        onChange={onFileChange}
        type="file"
        id="edit"
        accept="image/*"
      />
      <Submit onClick={onSubmit}>Submit</Submit>
      <Cancel onClick={onCancel}>Cancel</Cancel>
    </Wrapper>
  );
};

text, file 상태에 대한 초기값은 props로 전달받은 값, 즉 수정 전 원래 tweet의 내용이다. 

수정 상태를 나타내는 setIsEditing() 함수도 전달받아 로직이 완료되면 상태를 false로 바꿔준다. 

 

'Firebase > Nomadcoder Twitter' 카테고리의 다른 글

# 6.0 Deploy  (0) 2024.01.25
#5.0 User Avatar  (0) 2024.01.23
#3 Authentication  (0) 2024.01.02
#2 Setup  (0) 2023.12.27
#1. Introduction  (0) 2023.12.25