velopert
님의 책 리액트를 다루는 기술
을 정독하고 기본 예제를 쳐봤지만, 저의 기억력은.....금붕어라 금방 까먹더라구요.
그래서 책 마지막에 나오는 블로그 만들기
실습 예제를 다운로드하고, 프로젝트 구조를 파악하는 것을 이 포스팅의 목적으로 합니다.
구조를 파악하고 난 뒤에는 기능 단위로 저만의 간단한 실습을 정하고 진행할 예정입니다. 예를 들면 Redux + Saga
, Hooks
, 코드 스플리팅
, 서버 사이드 렌더링
이런 식으로 작은 단위를 나눠서 해볼 예정입니다!
그 후에는 하나의 토이 프로젝트
를 정하고 AWS EC2
에 배포하는 것까지 목표로 React
를 이용해서 웹을 만들 예정입니다.
참고로 저는 초판 버전이여서 이 프로젝트를 참고했습니다.
사실 구조만 파악할거라서 큰 의미는 없지만...
디렉토리 명 | 용도 |
---|---|
components | 리덕스 상태에 연결되지 않은 프리젠테이셔널 컴포넌트들을 모아 놓습니다. |
container | 리덕스 상태와 연결된 컨테이너 컴포넌트들이 있습니다. |
pages | 라우터에서 사용할 각 페이지 컴포넌트들이 들어 있습니다. |
lib | 백엔드 API 함수와 코드 스플리팅에 사용하는 asyncRoute가 있습니다. |
modules | Ducks 구조를 적용시킨 리덕스 모듈과 스토어 생성 함수가 있습니다. |
보니깐 기능(?) 단위로 나눠서 분류 해놓은거 같습니다. 예를 들면, 로그인 창이면 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
에 자주 사용하는 색상을 정의해서 불러다가 쓰고 있네요.
다른 컴포넌트도 이 구조와 비슷하게 되어있습니다.
Components
와 마찬가지로 기능 별로 디렉토리를 나눴네요. 보통 Container
라고 하면, class
형태로 작성되어 state
를 관리할 수 있는데, 여기선 state
관리를 redux
를 이용하기 때문에 functional 함수
로 작성한거 같습니다.
보통 Container
는 controller
역할을 수행한다고 알고 있습니다. 그래서 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
로 전달해 줍니다. 아까 말했듯이 중간다리 역할! 한다고 보시면 됩니다.
이 폴더에 대해서는 별로 설명할게 없습니다. 그냥 빈껍데기(?) 정도라고 생각하시면 편할거 같습니다.
정리해서 말하자면, 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;
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}`);
Redux
관련 모듈들이 있습니다! 아무래도 제가 제일 자신없어 하는 부분이고 공부해야 되는 부분입니다 ㅠㅠ
state
를 한곳에 모아 관리
하기 위해 만든 것이 Redux
라고 저는 이해하고 있습니다.
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는 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 공부하러 ㅎㅎ;