As i wish

[React SNS] React SNS 만들기 - 3 본문

React JS

[React SNS] React SNS 만들기 - 3

어면태 2019. 8. 6. 17:37

안녕하세요. 엄티입니다.

계속해서 React 를 사용해서 SNS 를 만들어 보겠습니다.

 

프로젝트 소개와 프로젝트 구성 요소는 제 전 포스팅을 참고해주세요.

React SNS 만들기 - 1

 

[React SNS] React SNS 만들기 - 1

안녕하세요. 엄티입니다. 오늘은 React 를 사용하여 간단한 SNS 를 만들어 볼껀데요. 제 포스팅은 '제로초' 님의 강의를 바탕으로 복습용으로 작성됩니다. 자세한 설명을 원하면 밑에 강의를 참조해 주세요. '제로..

eomtttttt-develop.tistory.com

참고로 제 포스팅은 '제로초' 님의 SNS 만들기 강좌 복습 포스팅입니다.

'제로초' 님의 React NodeBird SNS

 

React로 NodeBird SNS 만들기 - 인프런

리액트&넥스트&리덕스&리덕스사가&익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다. 중급 웹 개발 프레임워크 및 라이브러리 서비스 개발 Front End Back End Javascript React 온라인 강의

www.inflearn.com

 

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 시 --> 실행

 

리덕스를 왜 쓸까? - 1

 

리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악 (i) | VELOPERT.LOG

이 포스트는 리덕스의 리도 모르는 독자들을 대상으로 작성된 글입니다. 리덕스가 왜 필요한지 알아보고, 리덕스를 편리하게 사용하기 위한 발악을 한번 해보겠습니다. 리덕스 왜 쓸까? 리액트애서 애플리케이션을 만들 때, 기본적으로는 보통 하나의 루트 컴포넌트 (App.js) 에서 상태를 관리합니다. 예를들어서, 투두리스트 프로젝트에서는, 다음과 같은 구조로 상태가 관리되고 있죠. 리액트 프로젝트에서는 대부분의 작업을 할 때 부모 컴포넌트가 중간자 역할을 합니다

velopert.com

리덕스를 왜 쓸까? - 2 

 

리덕스(Redux)를 왜 쓸까? 그리고 리덕스를 편하게 사용하기 위한 발악 (ii) | VELOPERT.LOG

이 포스트는 이어지는 튜토리얼 입니다. 1편 을 먼저 읽고 오시길 바랍니다. 리덕스의 3가지 규칙 리덕스를 프로젝트에서 사용하게 될 때 알아둬야 할 3가지 규칙이 있습니다. 1. 하나의 애플리케이션 안에는 하나의 스토어가 있습니다. 하나의 애플리케이션에선 단 한개의 스토어를 만들어서 사용합니다. 사실, 권장되지는 않습니다. 여러개의 스토어를 만들고 싶다면 만들 수는 있습니다. 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분

velopert.com

참고로 리덕스에 대한 설명은 여타 블로그나 검색을 하면 상당히 많이 나와있습니다.

이를 참조하면 더욱 좋을것 같아요.

 

참고로 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 패턴으로 작성했습니다.

 

ducks 패턴

 

Redux의 ducks패턴에 대해서

Ducks patternRedux를 사용하는 어플리케이션을 구축하다 보면 기능별로 여러 개의 액션 타입과, 액션, 리듀서 한 세트를 만들어야 한다. 이들은 관습적으로 여러 개의 폴더로 나누어져서, 하나의 기능을 수정할 때는 이 기능과 관련된 여러 개의 파일을 수정해야 하는 일이 생...

guswnsxodlf.github.io

 

폴더 구조: reducers 폴더를 생성한다.

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 를 이해하기는 어렵습니다. 저도 처음에 이해하기가 어려웠고요. 그러니 꼭 더 검색을 통하여 개념을 꼭 다잡고 가세요!!!

 

Redux 소개 및 개념정리

 

Redux (1) 소개 및 개념정리

1-1. 리덕스 소개 리덕스는, 가장 사용률이 높은 상태관리 라이브러리입니다. 리덕스를 사용하면, 여러분이 만들게 될 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 더욱 효율적으로 관리 할 수 있습니다. 또한, 컴포넌트끼리 상태를 공유하게 될 때 여러 컴포넌트를 거치지 않고도 손쉽게 상태 값을 전달 할 수 있습니다. 추가적으로, 리덕스의 미들...

velog.io

 

각 단계 별로 보고 싶으시면 저의 commit message 를 보시면 됩니다.

react-sns commit message

 

eomttt/react-sns

React sns test by react-nodebird(zeroCho). Contribute to eomttt/react-sns development by creating an account on GitHub.

github.com

전체 코드를 확인하고 싶으시면 저의 깃허브를 참고해주세요.

위의 포스팅에서도 빠진 코드들이 많습니다

react-sns github

 

eomttt/react-sns

React sns test by react-nodebird(zeroCho). Contribute to eomttt/react-sns development by creating an account on GitHub.

github.com

 

폴더 구조

 

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 에 대하여 많은 이해를 해야합니다!!!

Comments