노드버드 리액트 섹션 2

릿·2023년 2월 9일
0

노드버드

목록 보기
3/4

1. 리덕스 설치와 필요성 소개

0. createStore취소선 강의공지

redux 사용 시 createStore에 취소선이 그어져있다는 제보를 해주시는 분들이 많습니다.
결론적으로는 createStore 그대로 쓰셔도 됩니다. 아무 문제 없습니다.
다만 redux 팀에서는 이제 redux를 쓰지 말고 @reduxjs/toolkit을 쓰라는 것으로 공식 입장을 정한 것 같습니다.
툴킷 적용을 원하신다면 제 깃헙 toolkit 브랜치 참고하시면 되겠습니다.

1. 어떤 상태관리 라이브러리를 사용할까?

  • 생산성을 생각한다면? 몹엑스 사용
  • 점유율을 고려한다면? 리덕스 사용
  • next.js에서는 next-redux-wrapper

2. next-redux-wrapper세팅

  1. 아래의 패키지 설치

    npm i next-redux-wrapper@6
    npm i redux
    npm i react-redux@7

  2. store폴더에 configureStore.js파일을 생성함

// store/configureStore.js

import { createWrapper } from "next-redux-wrapper";
import { createStore } from "redux";

const configureStore = () => {
  const store = createStore(reducer);
  return store;
};

// debug툴은 개발모드에서는 true로 놓고 쓰는 편이 코딩할 때 편함
const wrapper = createWrapper(configureStore, {
  debug: process.env.NODE_ENV === "development",
});

export default wrapper;
  1. _app.js에 가서 하이오더 컴포넌트로 감싸주기
_app.js

import wrapper from "../store/configureStore";

...
export default wrapper.withRedux(NodeBird);
  • 기존에 redux를 사용할 때, app.js에 Provider로 태그들을 감싸줬었지만 next.js는 알아서 감싸주기 때문에 할 필요가 없음

3. redux사용 권장이유

  • 데이터를 흩어지지 않게 하려면 부모컴포넌트가 필요, 중앙 데이터저장소 역할을 하는 것이 redux. (ex. contextAPI, redux, appollo, 몹엑스)
  • 에러추적이 쉽지만 코드량이 많음
  • contextAPI와의 차이 : 비동기를 다룰 때는 항상 실패에 대비해야 하기 때문에 요청/성공/실패를 다 구현해야 함. contextAPI를 쓰면 주로 컴포넌트 안에 useEffect에 요청을 함. 하지만 컴포넌트는 데이터 요청을 안하는 것이 좋기 때문에(강사 의견) 분리를 위해 redux, MobX권장

2. 리덕스의 원리와 불변성

  • 데이터의 수정, 추가, 삭제가 필요하면 action을 만들어서 dispatch하고 실제로 구현하는 reducer에서 데이터처리를 해주면 데이터가 바뀜
  • action을 만들 때마다 reducer도 하나씩 만들어줘야 함 -> 코드가 길어짐
  • action을 만들어주는 이유 : 기록이 남기 때문
  • reducer에서 항상 새로운 객체를 return하는 이유 : 변경 내역 추적을 위함
{
  ..state,
  name: action.data,
}
  • 위처럼 객체 return 시, 스프레드 연산자를 사용하는 이유 : 스프레드 연산자를 사용하면 참조관계가 되므로 메모리 절약이 됨. 개발모드 한정.
    (개발 모드 시, 히스토리를 계속 가지고 있지만 배포 모드일 때는 계속 메모리 정리가 됨)

3. 리덕스 실제 구현하기

1. 리덕스 예시구현

  • store는 state와 reducer를 일컬음
  1. reducer폴더에 index.js생성
// reducer/index.js
const rootReducer = (state, action) => {
  switch (action.type) {
  }
};

export default rootReducer;
  1. configureStore파일에 rootreducer를 import해줌
// configureStore.js
import reducer from "../reducers";

const configureStore = () => {
  const store = createStore(reducer);
  return store;
};
  1. 2번째 강의의 그림을 redux코드로 나타내면 아래와 같음
// reducer/index.js

const initialState = {
  name: "zerocho",
  age: 27,
  password: "babo",
};

const changeNickname = {
  type: "CHANGE_NICKNAME",
  data: "boogicho",
};

// (이전상태 ,액션) => 다음상태
const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case "CHANGE_NICKNAME":
      return {
        ...state,
        name: action.data,
      };
  }
};
  • 사용자가 사용하는 닉네임이 매번 달라질 경우, 매번 type, data를 만드는 건 비효율적임. 그래서 아래와 같이 동적으로 생성하게끔 바꿔줄 수 있음
// 이전
const changeNickname = {
  type: "CHANGE_NICKNAME",
  data: "boogicho",
};

// 변경
const changeNickname = (data) => {
  return {
    type: 'CHANGE_NICKNAME',
    data,
  }
}

changeNickname('zerocho');

2. 리덕스 프로젝트 구현

  • login, logout을 리덕스로 구현해보기
  1. reducers폴더의 index.js에 initialState, loginAction, rootReducer에 case문 작성
// reducers/index.js

const initialState = {
  user: {
    isLoggedIn: false,
    user: null,
    signUpData: {},
    loginData: {},
  },
  post: {
    mainPosts: [],
  },
};

export const loginAction = (data) => {
  return {
    type: "LOG_IN",
    data,
  };
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        user: {
          ...state.user,
          isLoggedIn: true,
          user: action.data,
        },
      };
  }
};
  1. AppLayout컴포넌트의 useState를 useSelector로 바꿔줌 (isLoggedIn값이 바뀌면 컴포넌트가 자동으로 리렌더링 됨)
  2. LoginForm과 UserProfile에 넘겨주던 props들을 지워줌
// AppLayout.js

import { useSelector } from "react-redux";

const AppLayout = ({ children }) => {
  const isLoggedIn = useSelector((state) => state.user.isLoddedIn);
  ...
  1. LoginForm컴포넌트에 action을 dispatch해주기 위해 useDispatch를 불러옴
// LoginForm.js

import { useDispatch } from "react-redux";

const LoginForm = () => {
  const dispatch = useDispatch();
  ...
  const onSubmitForm = useCallback(() => {
    console.log(id, password);
    dispatch(loginAction({ id, password }));
  }, [id, password]);

3. 미들웨어와 리덕스 데브툴즈

  • 현재 로그인, 로그아웃을 하면 개발자도구의 redux툴과 연동이 되어서 히스토리가 떠야하는데 연동이 되지 않고 있음, 연동을 위해 redux에 미들웨어를 붙여야 함
  • 미들웨어는 enhancer를 넣어서 연동할 수 있음
  • 리덕스 데브툴즈 설치

    npm i redux-devtools-extension

  • 아래와 같이 코드를 써주면 개발자도구의 리덕스 데브툴즈와 연동이 완료됨
// store/configureStore.js
import { createStore, applyMiddleware, compose } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";

const configureStore = () => {
  const middlewares = [];
  const enhancer =
    process.env.NODE_ENV === "production"
      ? compose(applyMiddleware(...middlewares))
      : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  return store;
};

...

4. 리듀서 쪼개기

  • 리듀서 코드가 길어진다면 파일을 나눌 수도 있음. 아래 코드를 user, post로 나누자
// reducers/index.js

const initialState = {
  user: {
    isLoggedIn: false,
    user: null,
    signUpData: {},
    loginData: {},
  },
  post: {
    mainPosts: [],
  },
};

export const loginAction = (data) => {
  return {
    type: "LOG_IN",
    data,
  };
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        user: {
          ...state.user,
          isLoggedIn: true,
          user: action.data,
        },
      };
  }
};
// user.js

export const initialState = {
  isLoggedIn: false,
  user: null,
  signUpData: {},
  loginData: {},
};

export const loginAction = (data) => {
  return {
    type: "LOG_IN",
    data,
  };
};

export const logoutAction = () => {
  return {
    type: "LOG_OUT",
  };
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        isLoggedIn: true,
        user: action.data,
      };
    case "LOG_OUT":
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    default:
      return state;
  }
};

export default reducer;
// post.js

export const initialState = {
  mainPosts: [],
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

export default reducer;
  • 리듀서가 함수이기 때문에 합치기 쉽지 않아서 combineReducers의 도움을 받아야 함.
  • combineReducers에 HYDRATE를 넣어주려면 index라는 항목을 넣어줘야 함
// reducers/index.js

import { HYDRATE } from "next-redux-wrapper";
import { combineReducers } from "redux";

import user from "./user";
import post from "./post";

const rootReducer = combineReducers({
  index: (state = {}, action) => {
    switch (action.type) {
      case HYDRATE:
        return { ...state, ...action.payload };
      default:
        return state;
    }
  },
  user,
  post,
});
  • 구조분해 사용도 가능함. 성능에는 미미한 차이가 있으므로 취향대로 쓸 것
const isLoggedIn = useSelector((state) => state.user.isLoggedIn);
// 구조분해해도 동일하게 작동함
const { isLoggedIn } = useSelector((state) => state.user);

5. 더미데이터와 포스트폼 만들기

  • 스타일드 컴포넌트 적용 안되는 문제: 스타일드 컴포넌트에 서버사이드렌더링 설정을 안해줬기 때문
  • post에 더미데이터를 만들어보자
export const initialState = {
  mainPosts: [
    {
      id: 1,
      User: {
        id: 1,
        nickname: "BonnieC",
      },
      content: "첫 번째 게시글 #해시태그 #익스프레스",
      Images: [
        {
          src: "https://user-images.githubusercontent.com/68591616/197114835-ac101ba7-4271-433d-a4c1-1be37b008f13.jpg",
        },
        {
          src: "https://user-images.githubusercontent.com/68591616/197114859-85dc51ce-cb85-4125-b635-3eb791706dc2.jpg",
        },
        {
          src: "https://user-images.githubusercontent.com/68591616/197114881-824baadb-4fd2-4c28-8f8f-df12e374bdfc.jpg",
        },
      ],
      Comments: [
        {
          User: {
            nickname: "nero",
          },
          content: "우와 개정판이 나왔군요~",
        },
        {
          User: {
            nickname: "hero",
          },
          content: "얼른 사고 싶어요~",
        },
      ],
    },
  ],
};
  • 왜 User와 Comments만 대문자인지? : DB 시퀄라이즈와 관계있음.
    id, content는 게시글 자체 설정이고, User, Images, Comments는 다른 정보와 합쳐서 주기 때문에 대문자로.
  • 데이터는 미리 서버쪽에 어떻게 보낼 건지 물어보는 게 좋음
  • type을 상수로 빼주면 중간에 오타가 나는 걸 막아줄 수 있음
const ADD_POST = "ADD_POST";

export const addPost = {
  type: ADD_POST,
};
  • map함수를 쓸 때, key값 설정 관련: index를 key값으로 쓰면 안됨. 특히 게시글이 지워질 수 있는 경우/게시글이 수정되거나 추가될 경우에는 더더욱. 백단에서 넘어오는 id값을 쓰는 편이 좋음
  • 컴포넌트 분리 팁 : 큰 범주로 나눌 수 있거나, map안에 있는 요소들은 분리해주면 좋음

1. 이미지 업로드 창 띄우기

  1. file타입 input을 만들어 ref를 지정해준다
  2. 버튼에 onClick함수를 연결해서 input에 지정된 ref.current.click()을 실행한다
...
const onClickImageUpload = useCallback(() => {
  imageInput.current.click();
}, [imageInput.current]);
...

...
<input type="file" multiple hidden ref={imageInput} />
<Button onClick={onClickImageUpload}>이미지 업로드</Button>

6. 게시글 구현하기

1. propTypes상세하게 작성하기

PostCard.propTypes = {
  post: PropTypes.object.isRequired,
};

// 아래와 같이 풀어서 쓸 수도 있음
PostCard.propTypes = {
  post: PropTypes.shape({
    id: PropTypes.number,
    User: PropTypes.object,
    content: PropTypes.string,
    createdAt: PropTypes.object,
    Comments: PropTypes.arrayOf(PropTypes.object),
    Images: PropTypes.arrayOf(PropTypes.object),
  }).isRequired,
};

2. 토글 구현시 코드 작성법

setLiked((prev) => !prev);

8. 이미지 구현하기

  • img태그에 onClick을 달아놓으면 웹접근성에 맞지 않음, 태그 안에 role='presentation'속성을 써주면 '클릭할 순 있지만 클릭할 필요가 없다'라는 의미

9. 이미지 캐루셀 구현하기(react-slick)

  • react-slick: react에서 캐루셀로 유명한 라이브러리 중 하나

    npm i react-slick

스타일드 컴포넌트의 문법

  • 자바스크립트 함수 호출법 중 하나인 태그드 템플릿 리터럴 함수 호출법을 사용함
const Overlay = styled.div``;

10. 글로벌 스타일과 컴포넌트 폴더 구조

1. 라이브러리 내의 클래스 스타일 변경방법

  • 원래 스타일드 컴포넌트로 스타일을 주게 되면 해당 태그의 클래스명은 난수화 되어 들어가게 되는데 createGlobalStyle을 사용하면 해당 태그에 클래스명이 그대로 들어가서 덮어 씌워짐
  1. 스타일드 컴포넌트의 우항을 createGlobalStyle로 쓰고, 변경할 클래스명을 적어 스타일을 변경
const Global = createGlobalStyle`
  .slick-slide {
    display: inline-block;
  }
`;
  1. 해당 태그는 return문 어디에 넣어도 무방함
...
  return (
    <Overlay>
      <Global />
      <Header>
...

2. 스타일드 컴포넌트 코드와 컴포넌트 분리방법

  • 해당 스타일을 재사용할 수 있으므로 컴포턴트와 스타일드 컴포넌트 코드를 분리하는 것도 나쁘지 않음
  1. 컴포넌트 폴더 내에 해당 ImagesZoom폴더를 만들고, 안에 index.js를 만들어 컴포넌트 코드를 작성함
  2. 같은 폴더 내에 styles.js를 만들어 스타일드 컴포넌트 코드를 분리함
// styles.js

import styled, { createGlobalStyle } from "styled-components";
import { CloseOutlined } from "@ant-design/icons";

export const Overlay = styled.div`
  position: fixed;
  z-index: 5000;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
`;
  1. index.js에 export해서 사용
// index.js

import React, { useState } from "react";
import PropTypes from "prop-types";
import Slick from "react-slick";

import { Overlay } from "./styles";

11. 게시글 해시태그 링크로 만들기

1. 해시태그 링크로 만드는 법

  • regexr.com 정규식 테스트 사이트
  • 해당 정규식을 split괄호 안에 넣어주면 원하는 의도대로 나오지 않을 수도 있기 때문에 슬래시 안의 정규식표현을 괄호로 한번 더 감싸줘야 함

0. 기타 팁

  • 서버 포트를 바꾸고 싶을 땐 package.json의 scripts에서 변경 가능
...
  "scripts": {
    "dev": "next -p 3060"
  },
...
profile
항상 재밌는 뭔가를 찾고 있는 프론트엔드 개발자

0개의 댓글