노드버드 리액트 섹션 3

릿·2023년 2월 17일
0

노드버드

목록 보기
4/4

3. Redux-saga연동하기

1. redux-thunk이해하기

  • redux-thunk: 리덕스 미들웨어, 리덕스가 비동기 액션을 디스패치할 수 있도록 도와주는 역할을 함
  • 원래 리덕스에서는 비동기 실행이 되지 않으나, redux-thunk를 쓰면 incrementAsync함수 부분에 여러 개의 동기 함수를 넣어 비동기로 실행할 수 있게 해줌
  • 미들웨어는 삼단 고차함수여서 항상 3개의 화살표를 가짐

1. 적용방법

  1. redux-thunk를 설치

    npm i redux-thunk

  2. store/configureStore.js파일에 thunkMiddleware를 import하고, middlewares상수 배열 안에 thunkMiddleware를 넣어줌
// store/configureStore.js

import thunkMiddleware from "react-thunk";

const configureStore = () => {
  const middlewares = [thunkMiddleware];
  ...

2. 사용 예시

  • action이 dispatch되는 것을 logging하는 미들웨어가 있다고 생각해보자
  • 리덕스에서 원래 action은 객체이나, 미들웨어에서는 action을 함수로 할 수 있음 (action이 함수일 때는 지연함수임)
// 미들웨어 생성
const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
  }
  return next(action);
};

// 아래의 middlewares배열에 추가해주면 됨
const configureStore = () => {
  const middlewares = [thunkMiddleware, loggerMiddleware];

3. redux-thunk vs redux-saga

1. redux-thunk

  • 장점 : 한 번에 여러가지를 디스패치 할 수 있게 해줌
  • 단점 : 그게 다임, 나머지는 본인이 다 구현해야 함

2. redux-saga특징

  • delay, debounce, throttle 등 다양한 내장기능 제공
  • takeLatest 기능 내장 (더블클릭이 되면 처음 건 무시하고 나중의 클릭에만 응답)
  • throttle: 중복요청을 막는 방법. 마지막 요청만 인정해주거나 시간당 횟수 제한 등이 가능 ⇒ (스크롤이벤트시 요청이 수백개 가는 것을 방지할 때 씀)

2. saga설치하고 generator이해하기

  1. 아까 설치한 redux-thunk는 삭제하고 redux-saga를 설치

    npm i redux-saga

  2. configureStore.js파일에 아래와 같이 코드를 변경해줌

// store/configureStore.js

import createSagaMiddleware from "@redux-saga/core";

import reducer from "../reducers";
import rootSaga from "../sagas";

const loggerMiddleware =
  ({ dispatch, getState }) => (next) => (action) => {
    console.log(action);
    return next(action);
  };

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  const middlewares = [sagaMiddleware, loggerMiddleware];
  const enhancer =
    process.env.NODE_ENV === "production"
      ? compose(applyMiddleware(...middlewares))
      : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  store.sagaTask = sagaMiddleware.run(rootSaga);
  return store;
};
  1. sagas폴더에 파일을 만들어 saga를 적용
// sagas/post.js
// addPost 작성예시

import { all, delay, put, takeLatest, fork, throttle } from "redux-saga/effects";

function addPostAPI(data) {
  return axios.post("/api/post", data);
}

function* addPost(action) {
  try {
    yield delay(1000);
    const id = action.data.id;
    yield put({
      type: ADD_POST_SUCCESS,
      data: {
        id,
        content: action.data,
      },
    });
    yield put({
      type: ADD_POST_TO_ME,
      data: id,
    });
  } catch (err) {
    yield put({
      type: ADD_POST_FAILURE,
      data: err.response.data,
    });
  }
}

function* watchAddPost() {
  yield takeLatest(ADD_POST_REQUEST, addPost);
}

export default function* postSaga() {
  yield all([
    fork(watchAddPost),
    ...
  ]);
}

1. generator

  • 자바스크립트에서 무한의 개념을 쓰고 싶을 때, 이벤트 리스너처럼 사용하고 싶을 때 generator를 많이 씀
  • generator는 함수 뒤에 .next()를 붙여줘야 함수가 실행됨
  • yield: 중단점, yield에 값을 할당하면 value에 해당 값이 찍히게 됨
const gen = function* () {
  console.log(1);
  yield;
  console.log(2);
  yield;
  console.log(3);
  yield 4;
}

const generator = gen()

gen().next()
// -> 1
// -> { value: undefined, done: false }
gen().next()
// -> 2
// -> { value: undefined, done: false }
gen().next()
// -> 3
// -> { value: 4, done: false }
gen().next()
// -> { value: undefined, done: true }

3. saga이펙트 알아보기

1. redux-saga effect설명

import { all, fork, take, call } from "redux-saga/effects";
import axios from "axios";

function logInAPI() {
  return axios.post("/api/login");
}

function* logIn() {
  try {
    const result = yield call(logInAPI);
    yield put({
      type: "LOG_IN_SUCCESS",
      data: result.data,
    });
  } catch (err) {
    yield put({
      type: "LOG_IN_FAILURE",
      data: err.response.data,
    });
  }
}

// 이벤트리스너 같은 역할을 함
function* watchLogin() {
  yield take("LOG_IN_REQUEST", logIn);
}

function* watchLogOut() {
  yield take("LOG_OUT_REQUEST", logOut);
}

function* watchAddPost() {
  yield take("ADD_POST_REQUEST", addPost);
}

export default function* rootSaga() {
  yield all([fork(watchLogin), fork(watchLogOut), fork(watchAddPost)]);
}
  • all: 안의 배열에 액션을 넣어주면 한번에 실행해줌
  • fork: 제너레이터 함수를 실행한다는 의미, 비동기 함수 호출
  • take: take의 안의 액션이 실행될 때까지 기다린다는 의미
  • call: 제너레이터 함수를 실행함(ex. API호출), 동기함수 호출, call앞의 yield는 await와 같은 역할을 함
  • put: == dispatch
  • 항상 effect앞에는 yield를 붙여줄 것

4. take, take시리즈, throttle알아보기

1. takeEvery

  • 위의 이벤트리스너 역할의 watchLogin, watchLogOut는 딱 한번만 실행되고 그 이후에는 사라짐, 그 때 while문을 사용하기도 함
function* watchLogin() {
	while (true) {
	  yield take("LOG_IN_REQUEST", logIn);
	}
}
  • 하지만 반복문 사용이 직관적이지 않기 때문에 takeEvery 사용을 권장
function* watchLogin() {
  yield takeEvery("LOG_IN_REQUEST", logIn);
}

2. takeLatest

  • takeLatest는 여러 번 클릭 했을 시, 첫번째 응답은 취소되고 마지막 응답만 실행됨 (요청은 여러 번 감), 그래서 서버에 데이터가 연달아 저장된 건 아닌지 검사가 필요함, 프론트에서는 대개 이 effect를 씀.

3. takeLeading

  • takeLeading은 takeLatest와는 반대로 첫번째 액션을 실행하고 나머지 액션은 무시됨

4. throttle

  • throttle은 아래와 같이 설정해놓으면 2초에 한번만 요청을 보낼 수 있다는 의미임, 디도스공격 방지용
function* watchAddPost() {
  yield throttle("ADD_POST_REQUEST", addPost, 2000);
}
  • delay: == setTimeout

5. throttle과 debouncing의 차이

  • 스크롤 이벤트 때 잦은 요청 방지 시 throttle사용
  • 검색어 단어가 완성되면 검색요청을 하려면 debouncing사용

5. saga쪼개고 reducer와 연결하기

1. 용도에 따라 파일 분리하기

  • reducer와 비슷하게 쪼개면 됨
  • reducer는 합칠 때 combine reducer로 합치지만 saga는 그럴 필요 없음

1. sagas/user.js의 경우

// sagas/user.js

import axios from "axios";
import { all, fork, takeEvery } from "redux-saga/effects";

function logInAPI() {
  return axios.post("/api/login");
}

function* logIn() {
  try {
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put({
      type: "LOG_IN_SUCCESS",
      data: result.data,
    });
  } catch (err) {
    yield put({
      type: "LOG_IN_FAILURE",
      data: err.response.data,
    });
  }
}

function* logOut() {
  try {
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put({
      type: "LOG_OUT_SUCCESS",
      data: result.data,
    });
  } catch (err) {
    yield put({
      type: "LOG_OUT_FAILURE",
      data: err.response.data,
    });
  }
}

function* watchLogIn() {
  yield takeEvery("LOG_IN_REQUEST", logIn);
}

function* watchLogOut() {
  yield takeEvery("LOG_OUT_REQUEST", logOut);
}

export default function* userSaga() {
  yield all([fork(watchLogIn), fork(watchLogOut)]);
}

2. sagas/post.js의 경우

// sagas/post.js

import axios from "axios";
import { all, delay, put, takeLatest, fork } from "axios";

function addPostAPI(data) {
  return axios.post("api/post", data);
}

function* addPost(action) {
  try {
    yield delay(1000);
    yield put({
      type: "ADD_POST_SUCCESS",
    });
  } catch (err) {
    yield put({
      type: "ADD_POST_SUCCESS",
      data: err.response.data,
    });
  }
}

function* watchAddPost() {
  yield takeLatest("ADD_POST_REQUEST", addPost, 2000);
}

export default function* postSaga() {
  yield all([fork(watchAddPost)]);
}

1. sagas/index.js의 경우

// sagas/index.js

import { all, fork } from "redux-saga/effects";

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

export default function* rootSaga() {
  yield all([fork(postSaga), fork(userSaga)]);
}
  • case문도 길어지면 combineReducer사용해서 쪼개는 게 좋음

7. 바뀐 상태 적용하고 eslint 점검하기

1. eslint rule을 엄격하게 하기

  1. 아래를 설치한다

    npm i -D babel-eslint
    npm i -D eslint-config-airbnb
    npm i -D eslint-plugin-import
    npm i -D eslint-plugin-react-hooks
    npm i -D eslint-plugin-jsx-a11y // 접근성

  2. .eslintrc에 "parser": "babel-eslint"를 써주면 최신 문법을 써도 에러가 나지 않음

  3. "extends": ["airbnb"]로 바꿔준다.

  4. rules에 아래와 같이 추가해준다.

  "rules": {
    "jsx-a11y/label-has-associated-control": "off",
    "jsx-a11y/anchor-is-valid": "off",
    "no-console": "off",
    "no-underscore-dangle": "off",
    "react/forbid-prop-types": "off",
    "react/jsx-filename-extension": "off",
    "react/jsx-one-expression-per-line": "off",
    "object-curly-newline": "off",
    "linebreak-style": "off"
  }

(npm run dev를 했을 때 eslint에러가 난다면 ide를 껐다가 다시 켜는 것도 방법 중 하나)

2. 에러 디버깅

  • eslint적용 중 import에 Parsing error가 난다면 "parser": "@babel/eslint-parser"로 바꿔주기

8. 게시글, 댓글 saga작성하기

1. 더미데이터 만들 때 유용한 라이브러리

1. shortid 라이브러리 설치

npm i shortid

  • 겹치기 힘든 id값을 만들어주는 라이브러리
  • 사용방법 : import한 뒤, 사용하는 곳에 .generate()를 해주면 됨
import shortId from "shortid";

const dummyPost = (data) => ({
  id: shortId.generate(),
  content: data,
  User: {
    id: 1,
    nickname: "BonnieC",
  },
  Images: [],
  Comments: [],
});

2. faker 라이브러리 설치

npm i -D faker@5
npm i -D @faker-js/faker // -> 위가 안될 경우, 이걸 설치할 것

  • 더미데이터를 만들어주는 라이브러리 (단점: 영어)

2. 불변성 객체 만들기

  • 원본 객체의 값을 변경하지 않도록 스프레드 연산자를 사용하여 얕은 복사 진행
// reducers/post.js

...
    case ADD_COMMENT_SUCCESS:
      const postIndex = state.mainPosts.findIndex(
        (v) => v.id === action.data.postId
      );
      const post = state.mainPosts[postIndex];
      post.Comments = [dummyComment(action.data.content), ...post.Comments];
      const mainPosts = [...state.mainPosts];
      mainPosts[postIndex] = post;
      return {
        ...state,
        mainPosts: [dummyPost(action.data), ...state.mainPosts],
        addPostLoading: false,
        addPostDone: true,
      };
...

10. immer도입하기

  • 리덕스 뿐만 아니라 react에서도 사용 가능, 데이터 불변성을 위해 사용하는 라이브러리이다.

    npm i immer

  • immer사용을 위해 .eslintrc의 rules에 rule 하나를 추가해주자.

"no-param-reassign": "off"
  • immer를 사용한 reducer함수 기본형태
import produce from 'immer';

const reducer = (state = initialState, action) => {
  return produce(state, (draft) => {
  	draft
  })
}
  • immer를 사용한 코드로 reducer함수를 바꿔보자.

1. 라이브러리 사용 전 코드

// reducers/post.js

const reducer = (state = initialState, action) => {
  switch (action.type) {
	...
    case ADD_COMMENT_REQUEST:
      return {
        ...state,
        addCommentLoading: true,
        addCommentDone: false,
        addCommentError: null,
      };
    case ADD_COMMENT_SUCCESS:
      const postIndex = state.mainPosts.findIndex(
        (v) => v.id === action.data.postId
      );
      const post = state.mainPosts[postIndex];
      post.Comments = [dummyComment(action.data.content), ...post.Comments];
      const mainPosts = [...state.mainPosts];
      mainPosts[postIndex] = post;
      return {
        ...state,
        addCommentLoading: false,
        addCommentDone: true,
      };
    case ADD_COMMENT_FAILURE:
      return {
        ...state,
        addCommentLoading: false,
        addCommentError: action.error,
      };
    default:
      return state;
  }
};
// reducers/user.js

case ADD_POST_TO_ME:
  return {
    ...state,
    me: {
      ...state.me,
      Posts: [{ id: action.data }, ...state.me.Posts],
    },
  };
case REMOVE_POST_OF_ME:
  return {
    ...state,
    me: {
      ...state.me,
      Posts: state.me.Posts.filter((v) => v.id !== action.data),
    },
  };

2. 라이브러리 사용 후 코드

// reducers/post.js

const reducer = (state = initialState, action) => {
  return produce(state, (draft) => {
    switch (action.type) {
	  ....
      case ADD_COMMENT_REQUEST:
        draft.addCommentLoading = true;
        draft.addCommentDone = false;
        draft.addCommentError = null;
        break;
      case ADD_COMMENT_SUCCESS:
        // 1. 게시글을 찾는다.
        const post = draft.mainPosts.find((v) => v.id === action.data.postId);
        // 2. 게시글에 새로운 코멘트를 하나 추가해주면 됨
        post.Comments.unshift(dummyComment(action.data.content));
        draft.addCommentLoading = false;
        draft.addCommentDone = true;
        break;
      case ADD_COMMENT_FAILURE:
        draft.addCommentLoading = false;
        draft.addCommentError = action.error;
        break;
      default:
        break;
    }
  });
};
// reducers/user.js

case ADD_POST_TO_ME:
  draft.me.Posts.unshift({ id: action.data });
  break;
case REMOVE_POST_OF_ME:
  draft.me.Posts = draft.me.Posts.filter((v) => v.id !== action.data);
  break;

11. faker로 실감나는 더미데이터 만들기

initialState.mainPosts = initialState.mainPosts.concat(
  Array(20)
    .fill()
    .map(() => ({
      id: shortId.generate(),
      User: {
        id: shortId.generate(),
        nickname: faker.name.findName(),
      },
      content: faker.lorem.paragraph(),
      Images: [
        {
          src: faker.image.image(),
        },
      ],
      Comments: [
        {
          User: {
            id: shortId.generate(),
            nickname: faker.name.findName(),
          },
          content: faker.lorem.sentence(),
        },
      ],
    }))
);
  • 이미지는 필요없고, 들어갈 공간정도는 남겨놓고 싶다하면 placeholder.com을 이용해보자

12. 인피니트 스크롤링 적용하기

  • 기타 팁: useEffect디펜던시에 빈 배열을 넣는다면 useUnmount와 같은 효과를 볼 수 있음

1. 무한스크롤 만들기

1. clientHeight와 scrollHeight를 이용하기

// pages/index.js

useEffect(() => {
  function onScroll() {
    console.log(
      window.scrollY,
      document.documentElement.clientHeight,
      document.documentElement.scrollHeight
    );
  }
  window.addEventListener("scroll", onScroll);
  return () => {
    window.removeEventListener("scroll", onScroll);
  };
});
  • scrollY : 얼마나 스크롤 내렸는지
  • clientHeight: 화면이 한번에 보이는 길이
  • scrollHeight: 화면 총 길이
  • 스크롤을 다 내렸을 때 scrollY + clientHeight를 하면 scrollHeight와 같다. 그렇게 하면 사용자가 문서를 맨 마지막까지 스크롤 했는지 확인할 수 있다.
  • 실무에서는 맨 밑에 도착하기 전에 페이지를 로딩할 준비를 하기 때문에 코드를 바꿔보자
// pages/index.js

// 스크롤이 바닥에서 300px 위쪽까지 도달하면 다음 10개 포스트 로딩을 준비
if (window.scrollY + document.documentElement.clientHeight >
      document.documentElement.scrollHeight - 300) {
   ...
}

2. 스크롤이벤트 시 REQUEST요청 한번만 보내기 : throttle적용

  • 실행해보면 리퀘스트가 한번이 아니라 여러번 실행된다. 이때는 saga의 takeLatest를 throttle 5초로 바꿔주자
// sagas/post.js

function* watchLoadPosts() {
  yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts);
}

3. 스크롤이벤트 시 REQUEST요청 한번만 보내기 : 좀 더 확실하게!

  • 이렇게 하면 리퀘스트를 한번만 실행하긴 하는데 이전에 보낸 리퀘스트를 취소하진 않는다. 리퀘스트를 한번만 보내기 위해서는? loadPostsLoading을 이용해보자.
// pages/index.js

useEffect(() => {
  function onScroll() {
    if (
      window.scrollY + document.documentElement.clientHeight >
      document.documentElement.scrollHeight - 300
    ) {
      if (hasMorePost && !loadPostsLoading) {
        dispatch({
          type: LOAD_POSTS_REQUEST,
        });
      }
    }
  }
  window.addEventListener("scroll", onScroll);
  return () => {
    window.removeEventListener("scroll", onScroll);
  };
}, [mainPosts, hasMorePost, loadPostsLoading]);

3. 무한스크롤 시, 메모리 관리방법 : react-virtualized

  • 무한스크롤로 인해 게시글 하나하나가 메모리를 잡아먹어 메모리가 터져버리는 상황이 발생할 수 있음 (특히 모바일!)
    그 때 react-virtualized라이브러리를 사용하면 좋다.
  • 원리 : 화면에 보이는 게시글 2~3개 외에 나머지는 메모리가 가지고 있음 ->
    스크롤이 내려가면 맨위 게시글은 사라지고, 밑에는 새로운 게시글 하나가 추가됨
profile
항상 재밌는 뭔가를 찾고 있는 프론트엔드 개발자

0개의 댓글