들어가기 앞서

velopert님의 책 리액트를 다루는 기술을 정독하고 기본 예제를 쳐봤지만, 저의 기억력은.....금붕어라 금방 까먹더라구요.

그래서 책 마지막에 나오는 블로그 만들기 실습 예제를 다운로드하고, 프로젝트 구조를 파악하는 것을 이 포스팅의 목적으로 합니다.

구조를 파악하고 난 뒤에는 기능 단위로 저만의 간단한 실습을 정하고 진행할 예정입니다. 예를 들면 Redux + Saga, Hooks, 코드 스플리팅, 서버 사이드 렌더링 이런 식으로 작은 단위를 나눠서 해볼 예정입니다!

그 후에는 하나의 토이 프로젝트를 정하고 AWS EC2에 배포하는 것까지 목표로 React를 이용해서 웹을 만들 예정입니다.

참고로 저는 초판 버전이여서 이 프로젝트를 참고했습니다.
사실 구조만 파악할거라서 큰 의미는 없지만...

프로젝트 구조

스크린샷 2019-09-06 오전 11.29.36.png

디렉토리 명 용도
components 리덕스 상태에 연결되지 않은 프리젠테이셔널 컴포넌트들을 모아 놓습니다.
container 리덕스 상태와 연결된 컨테이너 컴포넌트들이 있습니다.
pages 라우터에서 사용할 각 페이지 컴포넌트들이 들어 있습니다.
lib 백엔드 API 함수와 코드 스플리팅에 사용하는 asyncRoute가 있습니다.
modules Ducks 구조를 적용시킨 리덕스 모듈과 스토어 생성 함수가 있습니다.

Components

1. 하위 디렉토리

스크린샷 2019-09-06 오전 10.27.32.png

2. 설명

보니깐 기능(?) 단위로 나눠서 분류 해놓은거 같습니다. 예를 들면, 로그인 창이면 auth폴더 안에 컴포넌트들을 작성. 공통 컴포넌트들은 common 폴더 안에. 글 작성은 write 폴더 안에... 이런 식으로 나눠 놨습니다.

그리고 컴포넌트 파일들을 한번 확인 해봤습니다. 그 중에 소스코드가 가장 짧은 common 디렉토리에 있는 Tags.js의 소스 코드를 보겠습니다.

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import { Link } from 'react-router-dom';

const TagsBlock = styled.div`
  margin-top: 0.5rem;
  .tag {
    display: inline-block;
    color: ${palette.cyan[7]};
    text-decoration: none;
    margin-right: 0.5rem;
    &:hover {
      color: ${palette.cyan[6]};
    }
  }
`;

const Tags = ({ tags }) => {
  return (
    <TagsBlock>
      {tags.map(tag => (
        <Link className="tag" to={`/?tag=${tag}`} key={tag}>
          #{tag}
        </Link>
      ))}
    </TagsBlock>
  );
};

export default Tags;

presentational component이여서 함수형으로 작성이 되었네요. 리액트 컴포넌트 스타일링 방식 중 styled-component를 사용하고 있네요.

그래서 js만 이용해서 element를 생성하고 css까지 처리가 가능하네요! 정말 편리한거 같습니다.

styled-components에 대해 알고 싶으면 velopert님 포스팅을 참조하시면 좋습니다!

그리고 palette.js에 자주 사용하는 색상을 정의해서 불러다가 쓰고 있네요.

다른 컴포넌트도 이 구조와 비슷하게 되어있습니다.

Containers

1. 하위 디렉토리

스크린샷 2019-09-06 오전 10.47.08.png

2. 설명

Components와 마찬가지로 기능 별로 디렉토리를 나눴네요. 보통 Container라고 하면, class 형태로 작성되어 state를 관리할 수 있는데, 여기선 state 관리를 redux를 이용하기 때문에 functional 함수로 작성한거 같습니다.

보통 Containercontroller 역할을 수행한다고 알고 있습니다. 그래서 Presentational Component와 연결하기 위해 state, event, method 등을 연결하는 중간다리 역할을 생각하시면 됩니다.

PostViewerContainer.js 소스를 한번 보겠습니다.

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
import PostActionButtons from '../../components/post/PostActionButtons';
import { setOriginalPost } from '../../modules/write';
import { removePost } from '../../lib/api/posts';

const PostViewerContainer = ({ match, history }) => {
  // 처음 마운트될 때 포스트 읽기 API 요청
  const { postId } = match.params;
  const dispatch = useDispatch();
  const { post, error, loading, user } = useSelector(
    ({ post, loading, user }) => ({
      post: post.post,
      error: post.error,
      loading: loading['post/READ_POST'],
      user: user.user,
    }),
  );

  useEffect(() => {
    dispatch(readPost(postId));
    // 언마운트될 때 리덕스에서 포스트 데이터 없애기
    return () => {
      dispatch(unloadPost());
    };
  }, [dispatch, postId]);

  const onEdit = () => {
    dispatch(setOriginalPost(post));
    history.push('/write');
  };

  const onRemove = async () => {
    try {
      await removePost(postId);
      history.push('/'); // 홈으로 이동
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <PostViewer
      post={post}
      loading={loading}
      error={error}
      actionButtons={<PostActionButtons onEdit={onEdit} onRemove={onRemove} />}
      ownPost={user && user.id === post && post.id}
    />
  );
};

export default withRouter(PostViewerContainer);

소스를 보시면 알겠지만 onRemove, onEdit 함수를 PostViewer 컴포넌트에 props로 전달해 줍니다. 아까 말했듯이 중간다리 역할! 한다고 보시면 됩니다.

Pages

1. 하위 폴더

스크린샷 2019-09-06 오후 12.13.11.png

2. 설명

이 폴더에 대해서는 별로 설명할게 없습니다. 그냥 빈껍데기(?) 정도라고 생각하시면 편할거 같습니다.

정리해서 말하자면, Container 컴포넌트조합하여 하나의 페이지로 작성해둔 컴포넌트라고 말할 수 있습니다.

WritePage.js 소스만 보셔도 단번에 이해하실 수 있을겁니다.

import React from 'react';
import Responsive from '../components/common/Responsive';
import EditorContainer from '../containers/write/EditorContainer';
import TagBoxContainer from '../containers/write/TagBoxContainer';
import WriteActionButtonsContainer from '../containers/write/WriteActionButtonsContainer';
import { Helmet } from 'react-helmet-async';

const WritePage = () => {
  return (
    <Responsive>
      <Helmet>
        <title>글 작성하기 - REACTERS</title>
      </Helmet>

      <EditorContainer />
      <TagBoxContainer />
      <WriteActionButtonsContainer />
    </Responsive>
  );
};

export default WritePage;

lib

1. 하위 디렉토리

스크린샷 2019-09-06 오후 12.22.54.png

2. 설명

api 폴더 안에는 비동기적으로 호출하는 함수들을 정리 해놨습니다.
이 실습에서는 axios라는 비동기적으로 호출하여 데이터를 불러오는 라이브러리를 사용합니다. 마치 우리가 흔히 쓰던 ajax와 비슷합니다. axios의 장점에 대해선 여기에 간략하게 잘 정리되어 있으니 참고하시는 것도 나쁘지 않을듯 하네요!

client.js 에서는 axios 객체를 생성하고 설정할 수 있도록 하는 파일입니다.

import axios from 'axios';

const client = axios.create();

/*
  글로벌 설정 예시:

  // API 주소를 다른 곳으로 사용함
  client.defaults.baseURL = 'https://external-api-server.com/' 

  // 헤더 설정
  client.defaults.headers.common['Authorization'] = 'Bearer a1b2c3d4';

  // 인터셉터 설정
  axios.intercepter.response.use(\
    response => {
      // 요청 성공 시 특정 작업 수행
      return response;
    }, 
    error => {
      // 요청 실패 시 특정 작업 수행
      return Promise.reject(error);
    }
  })  
*/

export default client;

post.js 에서는 호출하는 함수들을 정리 해놨구요!

import qs from 'qs';
import client from './client';

export const writePost = ({ title, body, tags }) =>
  client.post('/api/posts', { title, body, tags });

export const readPost = id => client.get(`/api/posts/${id}`);

export const listPosts = ({ page, username, tag }) => {
  const queryString = qs.stringify({
    page,
    username,
    tag,
  });
  return client.get(`/api/posts?${queryString}`);
};

export const updatePost = ({ id, title, body, tags }) =>
  client.patch(`/api/posts/${id}`, {
    title,
    body,
    tags,
  });

export const removePost = id => client.delete(`/api/posts/${id}`);

modules

1. 하위 디렉토리

스크린샷 2019-09-06 오후 12.33.39.png

2. 설명

Redux 관련 모듈들이 있습니다! 아무래도 제가 제일 자신없어 하는 부분이고 공부해야 되는 부분입니다 ㅠㅠ
state를 한곳에 모아 관리하기 위해 만든 것이 Redux라고 저는 이해하고 있습니다.

  • Redux store에 state를 저장하고,
  • 변경이 일어나면 action 함수가 호출되어
  • reudx-saga를 이용해 비동기적으로 데이터를 받아와 Reducer에게 전해줍니다
  • 이 전달 받은 데이터를 토대로 우리가 정해놓은 Reducer 로직에 의해 state가 변경됩니다.
  • 그러면 Redux는 변경된 상태를 다시 View(컴포넌트들) 단에게 뿌려줍니다...

Reducer 관해서는 여기를 참조 했습니다.

그리고 구글에서 찾은 이미지인데, Redux를 조금이나마 이해하는데 도움이 되어서 올립니다.

다시 저희 프로젝트로 돌아와 index.js 파일을 보면

import { combineReducers } from 'redux';
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import loading from './loading';
import user, { userSaga } from './user';
import write, { writeSaga } from './write';
import post, { postSaga } from './post';
import posts, { postsSaga } from './posts';

const rootReducer = combineReducers({
  auth,
  loading,
  user,
  write,
  post,
  posts,
});

export function* rootSaga() {
  yield all([authSaga(), userSaga(), writeSaga(), postSaga(), postsSaga()]);
}

export default rootReducer;

이렇게 action과 saga를 등록하고 있는 것이 보이네요. 제가 쪼렙이라 소스 보기가 버겁네요 ㅠㅠ.. 아직 어떻게 돌아가는지 제대로 정립이 안되서 그런가봅니다...

App.js / index.js

App.js는 SPA를 위해서 라우트를 설정 해줍니다.

import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import WritePage from './pages/WritePage';
import PostPage from './pages/PostPage';
import { Helmet } from 'react-helmet-async';

const App = () => {
  return (
    <>
      <Helmet>
        <title>REACTERS</title>
      </Helmet>

      <Route component={PostListPage} path={['/@:username', '/']} exact />
      <Route component={LoginPage} path="/login" />
      <Route component={RegisterPage} path="/register" />
      <Route component={WritePage} path="/write" />
      <Route component={PostPage} path="/@:username/:postId" />
    </>
  );
};
export default App;

index.js 파일에서는 주로 Provider를 설정해주고요...
사실 Provider가 정확히 뭘하는지 모르겠습니다... 차차 공부하고 포스팅 하려구요 ㅎㅎ;

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';
import { HelmetProvider } from 'react-helmet-async';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(sagaMiddleware)),
);

function loadUser() {
  try {
    const user = localStorage.getItem('user');
    if (!user) return; // 로그인 상태가 아니라면 아무것도 안함

    store.dispatch(tempSetUser(user));
    store.dispatch(check());
  } catch (e) {
    console.log('localStorage is not working');
  }
}

sagaMiddleware.run(rootSaga);
loadUser();

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <HelmetProvider>
        <App />
      </HelmetProvider>
    </BrowserRouter>
  </Provider>,
  document.getElementById('root'),
);

serviceWorker.unregister();

serviceWorker.unregister();

정리

컴포넌트와 컨테이너 작성하는 것은 어렵지 않게 할 수 있을 것 같습니다!

그리고 axios를 이용해서 비동기로 호출하는 부분을 따로 빼놓는 것도 크게 어렵지 않을거 같구요.

다만 Redux, Redux-saga, action, actionType, dispatch....어후 머리에 안들어옵니다 ㅠㅠ 그래서 하나부터 차근차근 접근하기 위해 간단한 예제를 만들어 실습해보려 합니다.

제가 해야 할 일들을 아래와 같이 정리해봤습니다.

  • 비동기 호출을 제외하고! Redux 사용하는 예제 만들어보기!
    • Redux store, reducer, dispatch, action에 대해 학습 위주로
  • json server를 이용해 간단하게 백엔드 api를 만들고 비동기 호출을 이용한 Redux 예제 해보기!
    • Redux-saga 사용
    • Mobx 사용해보기

이 정도 있는거 같습니다...

그리고 돌아다니다가 우연히 우아한 형제 기술 블로그를 보게 되었는데, 작성해줘서 정말 정말 감사한 포스팅이였습니다.... 여러분들도 꼭 한번 보시길 강력히 추천드립니다.ㅎㅎ
저는 이만... Redux 공부하러 ㅎㅎ;