리액트 : 트위터 클론 정리

전역 후 처음으로 해본 실습이다. 노마드 코더의 강의를 따라서 진행했다. 리액트 위주의 강의이고 백엔드 구현을 위해 구글의 파이어베이스 서비스를 이용했다. 프론트 구현보다 파이어베이스를 가져다 쓰는데 더 오랜 시간과 힘이 들었다. 리액트를 이용하는 과정 자체는 이해하기 편했다. 군대에 있을 때 책으로 어느정도 훑고 온 덕인것 같다.

강의를 듣고 따라치는 시간보다도 오류가 났을 때 구글링해서 해결하고 처음 들어보는 개념을 찾아서 알아가는 과정이 더 오래걸렸다. 그게 니꼴라스가 우리에게 클론코딩을 추천하는 이유인 것 같긴 하다.
공식문서를 보면서 하는게 참 어렵다. 강의는 v8를 이용하지만 현재 바로 설치하면 v9가 설치된다. 처음에 firebase 구문이 강의와 달라서 직접 공식문서를 보며 했다. 결국 포기하고 v8로 다시 설치했다.

App.js에 userObj state를 만들고, 하위 컴포넌트들에 props로 뿌려서 계속 이용한다.


firebase.js

구글의 파이어베이스를 통해 Authentication 과 Database를 쉽게 구현할 수 있다. firebase.js 파일을 만들어 그 안에 import 해서 사용한다.

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/storage';
.
..
export const firebaseInstance = firebase;
export const authService = firebase.auth();
export const dbService = firebase.firestore();
export const storageService = firebase.storage();

이런식으로 파이어베이스 안의 여러 모듈들을 import 해올 수 있다. 함수들을 모아서 각각 export한다.

a. import와 export

react에서 변수를 사용하려고 할 때 중괄호 { }안에 표기하는 변수, 그리고 그냥 표기하는 변수가 있다. 이렇게 import할 때 { } 사용 여부는 변수를 보내주는 방식에 따라 달라진다

export default로 선언된 변수는 { } 없이 받아올 수 있으며 변수명이 달라도 된다. export로 선언된 변수들은 { }로 받아와야 하고, 변수명이 동일해야 한다. 변수명을 바꾸고 싶은 경우에는 as를 이용해서 바꿀 수 있다

페이지를 그리는 부분에서는 export default를, 모듈처럼 함수를 묶어놓은 부분에는 export를 주로 사용.

https://lily-im.tistory.com/21 (출처)

b. .env

필요한 키 값들을 복사해와 사용한다.

중요한 키들을 공개하기 싫어서, 혹은 코딩할 때 보기 좋으라고 저 키 값들을 다른 파일에 분리해두고 가져와 사용할 수 있다. 루트 디렉토리에.env 라는 이름의 파일을 하나 만든다.

변수 앞에 REACT_APP을 붙여야 한다.

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGIN_ID,
  appId: process.env.REACT_APP_APP_ID,
};

기존 코드 앞에 process.env. 을 붙여 사용한다.


Router.js

c. BrouserRouter

  • HTML5의 history API를 활용해서 업데이트.
  • 동적인 페이지에 적합.
    (서버에 있는 데이터들을 스크립트가 처리한 후 생성되어 전달)
  • 새로고침하면 경로를 찾지 못함. (이를 해결하기 위해 추가적인 세팅 필요. 페이지의 유무를 서버에 알려줘야 하며 서버 세팅 시 검색엔진에 신경써야 함)
  • github pages에서 설정하기 복잡 (배포가 복잡)

d. HastRouter

  • URL의 hash를 활용한 라우터.
  • 주소에 #가 붙는다.
  • 정적인 페이지에 적합. (미리 저장된 페이지가 그대로 보여짐)
  • 검색엔진으로 읽지 못한다.
  • 새로고침 해도 에러가 나지 않는다.
  • github pages에서 설정하기 간편 (배포가 간편)

이 강의에서는 마지막에 깃허브 페이지에 빌드하는것 까지 진행하기 때문에 hashRouter를 사용했다.

import {
  HashRouter as Router,Route, Switch,
} from 'react-router-dom';

as를 이용해서 import 해온 변수의 이름을 다르게 사용할 수 있다. HashRouter를 Router라는 이름으로 사용하고 있다.

e. Switch

<Switch>를 사용한 부분이 눈에 띈다. <Switch>를 함께 사용하는 이유는 path를 만족하는 컴포넌트를 하나 만나면 바로 매칭을 종료하고 렌더링시킨다.

import React from 'react';
import { 
	HashRouter as Router, Route, Switch 
} from 'react-router-dom';
import Auth from '../routes/Auth';
import Home from '../routes/Home';
import Navigation from './Navigation';
import Profile from '../routes/Profile';

const AppRouter = ({ refreshUser, isLoggedIn, userObj }) => {
	<Router exac path ="/">
    {isLoggedIn && <Navigation userObj={userObj} />}
    <Switch>
      {isLoggedIn ? (
        <>
          <Route exact path="/">
            <Home userObj={userObj} />
          </Route>
          <Route exact path="/profile">
            <Profile userObj={userObj} refreshUser={refreshUser} />
          </Route>
        </>
      ) : (
        <>
          <Route exact path="/">
            <Auth />
          </Route>
        </>
      )}
    </Switch>
  </Router>
};

export default AppRouter;

특히 이런 경우에 유용하게 쓰인다고 한다. switch를 사용하지 않았다면 무조건 에러 페이지도 함께 렌더링 될 것이다. 에러 페이지에는 path값이 없기 때문에 항상 만족한다.

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Routes = () => {
    return (
    	<Router>
          <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/movies" component={Movies} />
            <Route path="/reviews" component={Reviews} />
            <Route component={PageNotFound} />
          </Switch>
        </Router>
    );
};

위와 같이 <Route> 들을 <Switch> 로 감싸주면 에러가 발생했을 때 <PageNotFound> 가 나오게 되는데, 이는 첫번째로 매칭하는 path 값이 위에서 전부 없었기 때문이다.

https://baeharam.netlify.app/posts/react/why-switch-is-needed (출처)


Auth.js

firebase에 내장된 함수를 이용해 계정을 등록하고 로그인을 할 수 있다. Apps.js 에서 사용된 onAuthStateChanged() 메소드가 이벤트리스너처럼 실시간으로 변화를 감지한다. 파라미터로 firebase.User를 가져와 state에 설정하는 방식으로 유저정보를 받아온다.

이미 있는 계정인지 아닌지, 로그인된 상태인지 아닌지에 따라 보여줘야 하는 화면이 다르다. jsx에서 삼항연산자와 &&연산자를 빈번하게 쓰는 것 같다.

{error && <span className="authError">{error}</span>}

에러가 생겼을 때에만 에러메세지를 보여준다.

const toggleAccount = () => {
   setNewAccount((prev) => !prev);
};

<span onClick={toggleAccount} className="authSwitch">
	{newAccount ? 'Sign in' : 'Create Account'}
</span>

newAccount가 True이면 'Sign in', False이면 'Create Account'를 보여준다.

참거짓을 바꾸는 토글을 ((prev) ⇒ !prev) 로 간단하게 표현할 수 있다.

f. 비구조화 할당

객체 안에 있는 값을 추출해서 변수 혹은 상수로 바로 선언해 줄 수 있다.

const onChange = (e) => {
    const {
      target: { name, value },
    } = e;

    if (name === 'email') {
      setEmail(value);
    } else if (name === 'password') {
      setPassword(value);
    }
  };

input의 value에 변화가 생겼을 때 그 값들을 가져온다. e.target.name, e.target.value 를 이제 그냥 namevalue 라는 이름으로 쓸 수 있다.


Profile.js

user의 displayName을 바꾸거나 로그아웃을 할 수 있다.

g. useHistory

import { useHistory } from 'react-router-dom';
.
..
const history = useHistory();
const onLogOutClick = () => {
    authService.signOut();
    history.push('/');
  };

로그아웃을 한 후에 history.push('/')로 다시 돌아간다.

history 객체는 라우트로 사용된 컴포넌트에 전달되는 props 중 하나로, 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다. useHistory hooks를 이용해 history 객체 인스턴스에 접근할 수 있다.

history 객체에서go(n), goBack(), block(prompt), push(path, [state]) 등의 메소드를 사용할 수 있다.

push('/')가 실행되면 해당 path로 이동하고, history stack에 경로를 추가한다. 두번째 인수로 props를 담아 다음 페이지로 넘겨줄 수 있다.


Home.js

새로 글을 작성해 업로드하고, 파이어베이스 db에 저장된 'nweet'들을 불러와 보여준다.

import React, { useEffect, useState } from 'react';
import { dbService } from '../firebase';
import Nweet from '../components/Nweet';
import NweetFactory from '../components/NweetFactory';

const Home = ({ userObj }) => {
  const [nweets, setNweets] = useState([]); //서버에 저장한 글들

  useEffect(() => {
    dbService
      .collection('nweets')
      .orderBy('createdAt', 'desc')
      .onSnapshot((snapshot) => {
        const nweetArray = snapshot.docs.map((doc) => ({
          id: doc.id,
          ...doc.data(),
        }));
        setNweets(nweetArray);
      });
  }, []);

  return (
    <div className="container">
      <NweetFactory userObj={userObj} />

      <div style={{ marginTop: 30 }}>
        {nweets.map((nweet) => (
          <Nweet
            key={nweet.id}
            nweetObj={nweet}
            isOwner={nweet.creatorID === userObj.uid}
          />
        ))}
      </div>
    </div>
  );
};

export default Home;

useEffect - 컴포넌트가 화면에 처음 렌더링 될때 콜백함수를 실행한다.

.
..
dbService
      .collection('nweets')
      .orderBy('createdAt', 'desc')
      .onSnapshot((snapshot) => {
        const nweetArray = snapshot.docs.map((doc) => ({
          id: doc.id,
          ...doc.data(),
        }));
        setNweets(nweetArray);
      });
.
..

firebase의 메소드들을 이용해서 저장된 글들을 피드에 보여준다.

collection() - firebase db의 컬렉션 레퍼런스를 가져온다.

orderBy() - 만든 시각(createdAt)을 기준으로 내림차순(desc)으로 정렬한다.

onSnapShot() - 데이터의 변경을 실시간으로 읽어온다. nweetArray 라는 객체에 저장한다.

h. Spread Operator (전개구문)

배열 혹은 객체를 보기 좋게 다룰 수 있다. 새 객체 안에 id라는 속성을 넣고, data() 메소드로 가져온 데이터들을 다음 속성으로 복사해 넣는다.


NweetFactory.js

새 'nweet'을 생성하는 컴포넌트이다. 상위 커모넌트에서 userObj props를 받아와 사용한다.

이미지 파일을 업로드하기 위해 File API를 사용했다.

const onFileChange = (e) => {
    const {
      target: { files },
    } = e;
    const theFile = files[0];
    const reader = new FileReader();
    reader.onloadend = (finishedEvent) => {
      //이벤트리스너
      const {
        currentTarget: { result },
      } = finishedEvent;
      setAttachment(result);
    };
    reader.readAsDataURL(theFile); 
  };

브라우저 자체에서 file api를 지원해서 따로 설치할 필요는 없다고 함. readAsDataURL은 데이터를 URL로 만든다. onloadend(on을 붙여도 되고 안붙여도 되고) 는 이벤트 핸들러로, fileReader가 파일을 다 읽으면 결과(여기선 URL)를 state에 넣음.

load는 파일 읽기가 성공적으로 완료되었을 때, loadend는 성공적이든 아니든 완료되었을때 반환된다고 함.

const onSubmit = async (e) => {
    if (nweet === '') {
      return;
    }
    e.preventDefault();
    let attachmentUrl = '';
    if (attachment !== '') {
      const attachmentRef = storageService
        .ref()
        .child(`${userObj.uid}/${uuidv4()}`);
      const response = await attachmentRef.putString(attachment, 'data_url');
      attachmentUrl = await response.ref.getDownloadURL();
    }
    const nweetObj = {
      text: nweet,
      creatorID: userObj.uid,
      createdAt: Date.now(),
      attachmentUrl,
    };
    await dbService.collection('nweets').add(nweetObj);
    setNweet('');
    setAttachment('');
  };

attachment state의 값이 존재하면 nweetObj 객체로 만들어 컬렉션에 추가한다. 업로드한 파일에도 id를 붙여주기 위해서 uuid라는 라이브러리를 이용해 랜덤한 id를 생성해주었다.


깃허브 페이지를 이용해서 배포를 해보았다. 빌드를 하면 build라는 폴더가 새로 생기고, 깃허브 레포지토리에 gh-pages 라는 브랜치가 생긴다. 해당 브랜치에는 build의 파일들이 들어있다. 5분정도 기다리면 정상적으로 페이지가 뜨는것을 볼 수 있다.


재밌다. 강의를 따라하고 블로그에 정리하면서 이것저것 알아간 것들을 기록했다. 아무것도 아는 게 없고 앞으로 공부해야할 게 산더미라는 사실에 목이 턱턱 조이지만 열심히 하다보면 금방 늘거라고 믿는다. 이거 누구 보여주면 안될 것 같은데. 너무 뉴빈데.. 비공개글 같은거 없나 모르겠다.

좋은 웹페이지 즐겨찾기