일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- development
- 자바스크립트
- 영화
- nodejs
- 영화감상
- 주짓떼로
- 영화리뷰
- 개발자
- Express
- 프로그래밍
- 리액트
- 주짓떼라
- 엄티로드
- REACT
- 솔로드릴
- 하프가드
- Node
- 개발
- JavaScript
- Redux
- 웹개발
- graphQL
- 주짓수
- 노드
- 파이썬
- 클로즈가드
- git
- web
- 드릴
- 디자인패턴
- Today
- Total
As i wish
[React SNS] React SNS 만들기 - 3 본문
안녕하세요. 엄티입니다.
계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.
프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.
참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.
3번째 강좌에서는 redux를 적용했습니다.
Redux 는 쉽게 말해서 상태관리모듈? 이라고 한마디로 정의할 수 있을것 같습니다.
여러 state 가 각 component 에 퍼져있고 이 state 들을 받아서 component -> 자식 component -> 자식 component 에게 이런식으로 전달을 계속 해주죠. 근데 component 가 많아지면 관리하기가 상당히 힘들어집니다.
그래서 store 라는 저장소를 만들어서 각 component 가 각 store 에 state 정보를 읽고 있고 action이 발생하면 reducer 를 통하여 state 를 수정해주면 component 가 이를 바탕으로 새롭게 렌더링을 하죠.
action -> state 를 바꾸는 행동 ex) 로그인 액션
dispatch -> action 실행 ex) 로그인 액션 dispatch
reducer -> action 결과로 state 를 어떻게 바꿀지 정의한다. ex) 로그인 액션 dispatch 시 --> 실행
참고로 리덕스에 대한 설명은 여타 블로그나 검색을 하면 상당히 많이 나와있습니다.
이를 참조하면 더욱 좋을것 같아요.
참고로 redux와 react 는 별개 입니다. redux 는 어떻게 보면 하나의 모듈? 이라고 할 수도 있는데 react에 적용하면 잘 어울려서 많이들 적용 하는거죠.
따라서 react 프로젝트에 적용하기 위해서는 react-redux 모듈을 따로 또 설치해 주어야 합니다.
$ npm i redux react-redux
또한 저희 프로젝트는 next 를 함께 쓰고 있기 때문에 next와 redux 를 같이 사용하기 위해선 하나의 모듈을 더 설치해주어야 합니다.
$ npm i next-redux-wrapper
일단은 이런식으로 필요한 모듈을 설치해 주고 간단하게 reducer 부터 만들어보겠습니다.
reducer는 앞서 말한것 처럼 액션에 의하여 state 를 어떻게 변화 시킬지 정의하는 거죠.
여러 프로젝트에서는 action 을 따로 나누기도 하지만 이번 프로젝트에서는 reducer 와 action 을 한 파일에 정의하는 ducks 패턴으로 작성했습니다.
reducers/index.js
import { combineReducers } from 'redux';
import post from './post';
import user from './user';
const rootReducer = combineReducers({
user,
post,
});
export default rootReducer;
이렇게 각각 reducer 를 만들어 준다음 index.js 를 통해서 한번에 합쳐주기도 합니다.
한파일에 여러 reducer 를 다 정의해주어도 되지만 이러면 코드가 길어지고 가독성이 떨어지기 때문이겠죠?
reducers/user.js
const mockUser = {
nickname: 'MockUserRedux',
Post: [],
Followings: [],
Followers: [],
};
export const initialState = {
isLoggedIn: false,
user: null,
signUpData: {},
loginData: {},
};
// Action types
const SIGN_UP = 'SIGN_UP';
const SIGN_UP_SUCCESS = 'SIGN_UP_SUCCESS';
const LOG_IN = 'LOG_IN';
const LOG_IN_SUCCESS = 'LOG_IN_SUCCESS';
const LOG_IN_FAILURE = 'LOG_IN_FAILURE';
const LOG_OUT = 'LOG_OUT';
// Actions creators
export const signUpAction = (data) => {
return {
type: SIGN_UP,
payload: {
signUpData: data
}
}
};
export const loginAction = (data) => {
return {
type: LOG_IN,
payload: {
loginData: data
}
};
};
export const logoutAction = () => {
return {
type: LOG_OUT
}
};
// reducers
export default (state = initialState, action) => {
const {type, payload} = action;
switch(type) {
case SIGN_UP: {
return {
...state,
signUpData: payload.signUpData,
};
}
case SIGN_UP_SUCCESS:
return state;
case LOG_IN:
return {
...state,
isLoggedIn: true,
user: mockUser,
loginData: payload.loginData
}
case LOG_IN_SUCCESS:
return state;
case LOG_IN_FAILURE:
return state;
case LOG_OUT:
return {
...state,
isLoggedIn: false,
user: {}
}
default:
return state;
}
};
각각 action types, action creators, reducer 정의해 줍니다. 앞서 언급했던 것처럼 action types 들이 있고 이를 action creator 를 통하여 dispatch 시켜주면 reducers 가 이를 듣고 있다가 각각에 action type 에 맞게 state를 변경 시켜 줍니다.
여기서 state를 변경 시켜 줄 때에 핵심은 바로 불변성을 유지해야한다는 점인데요. 불변성을 유지하는 이유는 앞서 포스팅에서도 언급했던 것처럼 리액트가 화면을 리렌더링 하는 기준이 기존 state 와 전 state가 다를 때에 리렌더링을 하는데 객체같은 경우 참조가 같기 때문에 객체를 새로 만들어서 (참조를 다르게 한다) 전에 객체와 달라졌다고 알려주어야 합니다. 그렇기 때문에 기존 객체를 건드리지 말아야 하죠.
reducers/post.js
export const initialState = {
imagePaths: [],
mainPosts: [{
User: {
id: 1,
nickname: 'MockNickName',
},
content: 'First card',
img: 'https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726',
}],
};
// Action types
const ADD_POST = 'ADD_POST'; // Action nam
const ADD_DUMMY = 'ADD_DUMMY';
// Action creators
export const addPost = {
type: ADD_POST,
};
export const addDummy = {
type: ADD_DUMMY,
};
// reducers
export default (state = initialState, action) => {
const { type } = action;
switch (type) {
case ADD_POST: {
return {
...state,
};
}
case ADD_DUMMY: {
return {
...state,
};
}
default:
return state;
}
}
그 다음 이런 reducer를 react와 연결하는 것은 아주아주 간단합니다.
지난 포스팅에서 pages/_app.js 에 대하여 언급 했었는데 거기다가 추가만 해주면 됩니다.
pages/_app.js
import React from 'react';
import Head from 'next/head';
import PropTypes from 'prop-types';
import withRedux from 'next-redux-wrapper'; // With redux in next
import { applyMiddleware, compose, createStore } from 'redux';
import { Provider } from 'react-redux'
import reducers from '../reducers';
import AppLayout from '../components/AppLayout';
const NodeBird = ({ Component, store }) => {
// Using Provider link react with redux
return (
<Provider store={store}>
<Head>
<title>NodeBird</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.20.5/antd.css"/>
</Head>
<AppLayout>
<Component/>
</AppLayout>
</Provider>
);
};
NodeBird.propTypes = {
Component: PropTypes.elementType,
store: PropTypes.object
}
export default withRedux((initialState, options) => {
const middlewares = [];
const enhancer = compose(
applyMiddleware(...middlewares),
!options.isServer && typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== 'undefined' ? window.__REDUX_DEVTOOLS_EXTENSION__() : (f) => f, // For debuggin redux
);
const store = createStore(reducers, initialState, enhancer); // Create redux store
return store;
})(NodeBird);
이런 식으로 createStore 를 통해서 reducers 를 redux store에 정의해주고 Provider 를 사용해서 store를 넣어주면 자동으로 react 에서 redux 를 인식하고 동작하게 됩니다. Provider 를 넣으면 자식 컴포넌트들은 state (store 에 정의 되어있던) 들을 다 받을 수 있게 되죠.
또한 중간에 보이는 applyMiddleware 는 redux 에 없는 기능 들을 추가 하고 싶을 때에 적용해 줍니다. 위에 코드에서는 크롬 창에서 디버깅 할 수 있도록 추가해준거죠. 그 외에 redux 는 동기적인데 비동기적인 기능을 추가하고 싶을 때에 middleware를 추가해주기도 합니다.
그럼 이제 redux 를 사용하여 기존 프로젝트를 수정해 보도록 하겠습니다.
components/AppLayout.js
import React from 'react';
import { useSelector } from 'react-redux'
import Link from 'next/link';
import PropTypes from 'prop-types';
import { Menu, Input, Row, Col } from 'antd';
import LoginForm from '../components/LoginForm';
import UserProfile from '../components/UserProfile';
const AppLayout = ({ children }) => {
// Get from store
const { isLoggedIn } = useSelector(state => state.user);
return (
<div>
<Menu mode="horizontal">
<Menu.Item key="home"><Link href="/"><a>NodeBird</a></Link></Menu.Item>
<Menu.Item key="profile"><Link href="/profile"><a>Profile</a></Link></Menu.Item>
<Menu.Item key="mail"><Input.Search enterButton style={{ verticalAlign: 'middle' }}/></Menu.Item>
</Menu>
<Row gutter={8}>
<Col xs={24} md={6}>
{
isLoggedIn
? <UserProfile/>
: <LoginForm/>
}
</Col>
<Col xs={24} md={12}>{children}</Col>
<Col xs={24} md={6}><Link href="https://www.zerocho.com"><a target="_blank">Made by ZeroCho</a></Link></Col>
</Row>
</div>
)
};
AppLayout.propTypes = {
children: PropTypes.node
}
export default AppLayout;
위의 코드에서 useSelector 를 사용한것이 보이는데 이는 reducers 에 정의되어 있던 state 를 가져오는 것이죠. state.user 를 가져왔기 때문에 reducers/user.js 안에 있는 state 를 가져왔다고 볼 수 있습니다.
components/LoginForm.js
import React, { useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Link from 'next/link';
import { Form, Input, Button } from 'antd'
import { loginAction } from '../reducers/user';
// Custom hook
export const useInput = (initValue = null) => {
const [value, setter] = useState(initValue);
const handler = useCallback((e) => {
setter(e.target.value);
}, []);
return [value, handler];
}
const LoginForm = () => {
const dispatch = useDispatch();
const [id, onChangeId] = useInput('');
const [pass, onChangePass] = useInput('');
const onSubmitForm = useCallback((e) => {
e.preventDefault();
dispatch(loginAction({
id,
pass
}))
}, [id, pass]);
return (
<>
<Form onSubmit={onSubmitForm} style={{ padding: '10px' }}>
<div>
<label htmlFor="user-id">ID</label>
<br/>
<Input name="user-id" required value={id} onChange={onChangeId}/>
</div>
<div>
<label htmlFor="user-pass">Password</label>
<br/>
<Input name="user-pass" type="password" required value={pass} onChange={onChangePass}/>
</div>
<div style={{marginTop: '10px' }}>
<Button type="primary" htmlType="submit" loading={false}>LogIn</Button>
<Link href="/signup"><a><Button>SignUp</Button></a></Link>
</div>
</Form>
</>
)
}
export default LoginForm;
한가지 더 예를 들어서 LoginForm.js 에서는 useDispatch() 를 사용하였는데 이는 앞서 언급했던 action 을 dispatch 하기 위해 가져온 것이죠.
위의 코드에서 onSubmitForm 을 보면 로그인 버튼을 누르면 dispatch(loginAction({id, pass})) 를 하게 되어있는데 loginAction 은 reducers/user 에서 가져 온 action creators 중 하나라고 할 수 있습니다.
그럼 대략 적으로 예를 들어 설명해 보면
사용자가 로그인 버튼을 누릅니다 -> 미리 정의 해 놓은 action 을 dispatch 합니다. -> reducer 가 이를 감지하고 action type 을 찾아서 그에 맞게 state 를 변경합니다. -> state 는 isLoggedIn 은 true, user 는 mockUser, loginData 는 dispatch 한 action 에서 넘겨준 {id, pass} 로 변경해줍니다.
처음에 언급했던 action, dispatch, reducer 가 정의대로 잘 실행 된것을 볼 수 있습니다.
여기서 isLoggedIn 이 변경되었기 때문에 useSelector() 로 isLoggedIn 을 구독하고 있던 component 들은 그에 맞게 리렌더링 되겠죠?
components/UserProfile.js
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Avatar, Button } from 'antd'
import { logoutAction } from '../reducers/user';
const UserProfile = () => {
const dispatch = useDispatch();
const { user } = useSelector(state => state.user);
const onLogout = useCallback(() => {
dispatch(logoutAction());
}, []);
return (
<Card
actions={[
<div key="twit">짹짹<br />{user.Post.length}</div>,
<div key="following">팔로잉<br />{user.Followings.length}</div>,
<div key="follower">팔로워<br />{user.Followers.length}</div>,
]}
>
<Card.Meta
avatar={<Avatar>{user.nickname[0]}</Avatar>}
title={user.nickname}
/>
<Button onClick={onLogout}>LogOut</Button>
</Card>
);
};
export default UserProfile;
여기서는 logoutAction 을 dispatch 하고 있습니다.
이렇게 하면 dispatch 를 통하여 action creator 를 불러주고 reducer는 그에 대한 action type에 맞게 state를 변경해주는데, 이는 reducers 에 이미 정의한 대로 변형 되죠. LOG_OUT action 은 isLoggedIn 을 false 로, user: {} 로 변경해 줍니다.
pages/index.js
import React from 'react'; // eslint 에서 react 쓰면 import 하라고 명시됨
import { useSelector } from 'react-redux';
import PostForm from '../components/PostForm';
import PostCard from '../components/PostCard';
const Home = () => {
const { isLoggedIn } = useSelector(state => state.user);
const { mainPosts } = useSelector(state => state.post);
return (
<div>
{isLoggedIn && <PostForm />}
{
mainPosts.map((v) => {
return <PostCard key={v} post={v} />
})
}
</div>
);
};
export default Home;
여기서도 useSelector를 통해서 state 를 가져오고 있습니다. 앞서 isLoggedIn state 가 action 에 따라 바뀜에 따라 여기도 렌더링이 다시 되겠죠??
이처럼 redux 를 사용하면 아주 편하게 각 컴포넌트들에 상태를 관리하고 변경 할 수 있습니다. 초기에 설계하는데에는 시간이 조금 걸리지만 익숙해지면 이도 금방 적응 되고, 리엑트 프로젝트를 쉽게 관리할 수 있게 됩니다.
제 포스팅으로는 한번에 redux 를 이해하기는 어렵습니다. 저도 처음에 이해하기가 어려웠고요. 그러니 꼭 더 검색을 통하여 개념을 꼭 다잡고 가세요!!!
각 단계 별로 보고 싶으시면 저의 commit message 를 보시면 됩니다.
전체 코드를 확인하고 싶으시면 저의 깃허브를 참고해주세요.
위의 포스팅에서도 빠진 코드들이 많습니다
package.json
{
"name": "react-nodebird-front",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "next",
"build": "next build",
"start": "next start"
},
"author": "TeiEom",
"license": "ISC",
"dependencies": {
"antd": "^3.20.5",
"next": "^9.0.2",
"next-redux-wrapper": "^3.0.0-alpha.3",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^7.1.0",
"redux": "^4.0.4"
},
"devDependencies": {
"eslint": "^6.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.6.1",
"nodemon": "^1.19.1",
"webpack": "^4.36.1"
}
}
다음엔 redux 에서 비동기 및 다양한 기능들을 하기 위해 redux-saga 를 적용해 볼 예정입니다. redux-saga 는 redux 보다 조금 더 어렵습니다. 꼭 redux 에 대하여 많은 이해를 해야합니다!!!
'React JS' 카테고리의 다른 글
[React SNS] React SNS 만들기 - 4 - 2 (redux saga 적용) (0) | 2019.08.12 |
---|---|
[React SNS] React SNS 만들기 - 4 - 1 (redux saga 란, airbnbStyle applied) (0) | 2019.08.12 |
[React SNS] React SNS 만들기 - 2 (0) | 2019.07.30 |
[React SNS] React SNS 만들기 - 1 (0) | 2019.07.29 |
[React JS] BookMark - 1 (0) | 2019.07.18 |