반응형

버튼 태그에 setCount 메소드를 onClick 이벤트에 등록해주는데,

setCount의 인자로 함수를 보냅니다.

+ 버튼을 클릭하면 이전상태로부터 1을 증가시키는 함수가 setCount의 인자로 들어가게 됩니다.

 

간단한 경우에는 useState를 쓰는 것이 더 좋다고 생각하지만

현재의 상태에 액션을 받아서 새로운 상태로 갱신하는(reducer) 형태인 경우 useReducer를 사용해야 합니다.

  1. 다수의 하윗값을 포함하는 정적 로직을 만드는 경우
  2. 다음 state가 이전 state에 의존적인 경우

이러한 경우에 useState보다 useReducer가 선호됩니다.

 

useReducer는 첫 번째 인자로 reducer 함수를 받고, 두 번째 인자로는 initialState 객체를 받습니다.

import React, { useReducer } from 'react';

const initialState = { count: 0 };

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count : {state.count}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
};

export default Counter;

 

useReducer는 state와 dispatch를 반환하기 때문에 dispatch로 액션을 보내는 함수를 onClick 이벤트에 등록해 준 것을 알 수 있습니다. 그러면 RESET 버튼은 어떻게 구현할까요?

import React, { useReducer } from 'react';

const init = (initialCount) => {
  return { count: initialCount };
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
};

const Counter = ({ initialCount }) => {
  const [state, dispatch] = useReducer(reducer, initialCount, init);

  return (
    <>
      Count : {state.count}
      <button
        onClick={() => dispatch({ type: 'reset', payload: initialCount })}
      >
        RESET
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
};

export default Counter;

init 함수를 만들어 useReducer의 세 번째 인자로 전달해 주면 됩니다.

 

잘 동작합니다!

출처: https://clearwater92.tistory.com/26

반응형
반응형

Vue.js에서 computed 프로퍼티는 매우 유용하게 사용된다. 그러나 처음 Vue.js를 시작할때 computed와 watch가 모두 반응형이라는 키워드과 관련이 있기 때문에 이 둘을 혼동하곤 했다. Vue.js의 강점을 잘 살려서 코딩을 하기위해 이 두가지 키워드를 잘 알고 있어야 한다.

Computed — 반응형 getter

computed를 한마디로 얘기하자면 “반응형 getter”이다. 아래 예시를 보자. vue.js 공식가이드 문서에 나오는 예시와 동일한 로직을 옮긴것이다.

<template>
  <div>
    <p>원본 메시지: "{{ message }}"</p>
    <p>역순으로 표시한 메시지: "{{ reversedMessage }}"</p>
  </div>
</template>

<script>
export default {
  name: 'test',
  data(){
    return {
      message: '안녕하세요'
    }
  },
  computed: {
    reversedMessage: function () {
      return this.message.split('').reverse().join('')
    }
  }
}
</script>

computed 프로퍼티를 보면 reverseMessage 라는 프로퍼티에 값으로 익명함수가 할당되어있다. computed에 정의하는 이 익명함수는 반드시 값을 리턴하도록 작성되야한다.

getter

computed의 reverseMessage 프로퍼티가 정될때 내부적으로는 Object.defineProperty를 통해 정의되며, 이때 익명함수가 getter로 설정된다. reverseMessage 를 함수가 아니라 일반 객체처럼 사용할 수 있는점과 호출될때만 계산이 이루어지고, 계산결과가 캐싱되는 특성이 생기게 된것은 getter의 특성덕분이다(이는 methods와의 차이를 유발하는 지점이기도 하다). 하지만 바로 이점 때문에 값이 변하게 되어도 캐싱때문에 변경된 값을 인지하지 못하는 단점이 생기게된다.

반응형(reactive)

Vue.js 는 이 단점을 상쇄하고 반응형을 구현하기 위해 특별한 장치를 한다. getter 함수 내에 속한 프로퍼티의 변경여부를 추적하는 것이다.(마이구미님 글 참고) 위 예시에서는 message 를 감시하고 있다가 message의 값이 변경되면 reverseMessage 를 다시 계산한다. 결국, computed는 사용하기 편하고, 자동으로 값을 변경하고 캐싱해주는 아주 끝내주는 “반응형 getter”라 부를 수 있겠다. (반응형은 Computed뿐 아니라 Vue.js 의 전반의 주요한 특징으로 볼 수 있다.)

Watch — 반응형 콜백

변경을 감시(watch)한다는 점 때문에 computed와 watch를 혼동할 수 있다.걱정할 필요는 없다. computed에 비해 watch는 단순하고 이해하기 쉽기 때문이다. watch는 Vue 인스턴스의 특정 프로퍼티가 변경될때 지정한 콜백함수가 실행되는 기능이다. 위 예시를 응용한다면 아래와 같을 것이다.

<template>
  <div>
    <p>원본 메시지: "{{ message }}"</p>
    <p>역순으로 표시한 메시지: "{{ reversedMessage }}"</p>
  </div>
</template>

<script>
export default {
  name: 'test',
  data(){
    return {
      message: '안녕하세요',
      reversedMessage: ''
    }
  },
  watch: {
    message: function (newVal, oldVal) {
      this.reversedMessage = newVal.split('').reverse().join('')
    }
  }
}
</script>

watch를 정의한 부분(17~21)을 보면 message 프로퍼티에 익명함수가 할당되어있다. 이 익명함수가 콜백함수 역할을 할 것이고, message 프로퍼티가 변경되면 변경된 값을 콜백함수의 첫번째 인자로 전달하고, 이전 값을 두번째 인자로 전달하여 실행한다. computed가 새 프로퍼티를 생성하고 그것의 getter 로 익명함수를 설정되는 것과는 달리 watch는 아무 프로퍼티도 생성하지 않고 익명함수는 단순히 콜백함수로의 역할을 한다. watch에 명시된 프로퍼티는 감시할 대상을 의미할 뿐이다.

어떻게 사용할 것인가

  • 위의 예시처럼 인스턴스의 data에 할당된 값들 사이의 종속관계를 자동으로 세팅하고자 할때는 computed로 구현하는것이 좋다. 그러니까 reverseMessage  message 값에 따라 결정되어진다. 이 종속관계가 조금이라도 복잡해지면 watch로 구현할 경우 중복계산이 일어나거나 코드 복잡도가 높아질 것이다. 이는 오류도 더 많이 발생시킬 것이다.
  • watch는 특정 프로퍼티의 변경시점에 특정 액션(call api, push route …)을 취하고자 할때 적합하다.
  • computed의 경우 종속관계가 복잡할 수록 재계산 시점을 예상하기 힘들기 때문에 종속관계의 값으로 계산된 결과를 리턴하는 것 외의 사이드 이펙트가 일어나는 코드를 지양해야한다.
  • 더 쉽게 판단하는 방법: 만약 computed로 구현가능한 것이라면 watch가 아니라 computed로 구현하는것이 대게의 경우 옳다.

참고 문서

출처: https://jeongwooahn.medium.com/vue-js-watch%EC%99%80-computed-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%99%80-%EC%82%AC%EC%9A%A9%EB%B2%95-e2edce37ec34

반응형
반응형

 Native Event? 

 

Native! react에서는 참 많이 들어본 말이지만 vue에서의 native는 생소하다. 실제로 검색을 해봐도 react에 관련된 내용만 잔뜩 나오지 vue.js native에 대해서는 자료가 적다. 공식문서를 살펴보면 커스텀이벤트 페이지에서 다루고 있으며, 컴포넌트에 이벤트를 바인딩할 때 사용한다.

 

 

공식사이트의 문구를 살펴보면 컴포넌트에서 루트엘리먼트의 네이티브 이벤트를 직접 감지하고 싶은 경우, v-on에 사용한다고 설명되어 있다. 해당 문구만으로는 네이티브가 정확히 어떤일을 하는지 파악하기 어렵다. 기능과 함께 vue에서 이야기하는 native가 무엇인지에 대한 고찰을 정리하려한다. 개인적으로 찾아본 자료들을 조합한 고찰이니 완전히 정답이라고 할 수 없다. 기능의 대한 설명을 제외하곤 정확한 정보가 아닐수도 있다는 점 참고!

 

 

 


 

 v-on의 native는 어떤 기능을 할까? 

 

네이티브는 v-on 이벤트 장식자로 취급되는데 컴포넌트의 루트 요소 위에 있는 네이티브 이벤트를 호출한다. 선언법은 아래와 같다.

<컴포넌트명 @이벤트핸들러.native="이벤트함수명"></컴포넌트명>

 

컴포넌트에서 이벤트를 직접 호출하고 싶은 경우에 사용한다. 아래와 같이 하위 컴포넌트에 버튼을 두고 상위컴포넌트로 감싸고 있는 형태에, 하위컴포넌트의 이벤트를 전달하기 위해서는 $emit을 이용하는것이 보통이다. 

하지만 상위컴포넌트의 이벤트를 호출하고 싶을때는 어떻게 해야할까? 하위컴포넌트에 해당 이벤트를 작성해도 이벤트는 작동하지 않는다. 그 이유는 vue 구성요소는 기본 요소에 바인딩하려고 하기때문이다. 이 문제를 해결하기 위해 존재하는 장식자가 바로 네이티브다. 네이티브를 붙인다면 기본요소를 무시하고 상위컴포넌트에 있는 이벤트를 실행시킨다. 코드로 결과를 확인해보자!

 

<!-- app -->

<template>
  <div id="app">
  <h5>app 이벤트</h5>
  <button @click="clickTest">appBtn</button>
  <h5>클릭 이벤트</h5>
  <Test @click="clickTest"></Test> <!-- 하위컴포넌트 -->
  <h5>클릭 native 이벤트</h5>
  <Test @click.native="clickTest"></Test> <!-- 하위컴포넌트 -->
  </div>
</template>

<script>
import Test from '../src/components/test.vue'

export default {
  name: 'App',
  methods:{
  	clickTest(){
      alert("Clicked");
    }
  },
  components: {
    'Test' : Test
  }
}
</script>
<!-- test(하위컴포넌트) -->

<template>
    <div>
        <button>componentBtn</button>
    </div>
</template>

이미지를 누르면 gif가 실행됩니다


 vue의 Native event가 무엇일까? (개인적고찰) 

 

Native의 단어 뜻은 본래의, 고유의, 토착의 라는 뜻을 가졌으며 vue에서의 네이티브는 Native DOM event(네이티브 돔 이벤트)를 의미한다. 네이티브 돔 이벤트을 검색해보면 제이쿼리가 많이 나오는데, 제이쿼리에서 사용하는 대부분의 메소드에 해당하기 때문이다. 제이쿼리의 이벤트 방법에 대해서는 해당링크를 통해 읽어보는걸 추천한다. 제이쿼리는 dom 요소를 직접적으로 참조하여 가져온 후 이벤트헨들러로 dom 이벤트를 처리한다. 이런 내용을 통해 생각을 정리해보았을때, 네이티브 이벤트라는것은 dom 요소를 직접 참조하여 이벤트를 실행시키는것이 라는 뜻이지 않을까? 이 내용을 착안해서 vue는 루트엘리먼트(상위컴포넌트)의 요소를 직접 참조하여 이벤트한다 라는 느낌으로 사용하는 것 같다. 몰론 개인적인 생각이므로 맞는지는 정확하게 확인하지 못했다.😂 vue는 검색해도 내용이 너무 안나온다.

 

왜 html 리스너를 사용하는가

 

왜 내가 제이쿼리를 보고 네이티브를 이해했냐면, vue는 html 리스너를 사용하기 때문이다. 기존에 있는 일반 이벤트 처리와 방식이 크게 다르지 않는 이유도 여기에 있다. 어림짐작으로 v-on은 js의 addEventListener 역할을 하는게 아닐까 싶다. vue는 새로운 프레임워크지만 기존의 소비자가 어렵지않게 사용할 수 있도록 기존의 사용법을 많이 착안해서 제작한것같다. 정말 좋은쪽으로 발전한 프레임워크다😍

 

 

※ addEventListener : 특정 함수에 이벤트핸들러를 부착시키는 기능. 하나의 요소에 다양한 이벤트 핸들러를 추가할 수 있다. click, keyup등 이벤트등 많은 이벤트를 해당 기능으로 등록할 수 있다.

element.addEventListener('이벤트명(click,select 등)', 지정할 콜백함수, 이벤트버블링, 캡쳐링 실행여부(true, false));

 

※  dom에 관련되서는 해당링크를, 이벤트객체에 대해서는 해당링크를 더 읽어보자!

 

 

 

 

 

 

해당문서에 대해 오류가 있다면 댓글로 지적부탁드립니다 :)

 

 

 

 

참고문서 : http://www.tcpschool.com/javascript/js_event_eventListenerCall

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

참고문서 : https://kr.vuejs.org/v2/guide/components-custom-events.html

 

커스텀 이벤트 — Vue.js

Vue.js - 프로그레시브 자바스크립트 프레임워크

kr.vuejs.org

참고문서 : https://chlolisher.tistory.com/10

 

[ JavaScript ] Event의 종류 : addEventListener, removeEventListener 메서드

addEventListener : 특정 이벤트가 발생했을 시, 특정 함수를 실행할 수 있게 해주는 기능.  addEventListener으로 등록할 수 있는 이벤트 중 자주 쓰이는 이벤트 목록 이벤트 명 설명 change 변동이 있을 때

chlolisher.tistory.com

참고문서 : https://codepen.io/gambo/pen/JrPeqN

 

Vue.js Native Events

...

codepen.io

 

읽으면 좋은 문서 : https://programmer-seva.tistory.com/63

 

[스터디_자바스크립트] 50. DOM 심화 - DOM 이벤트

1. DOM 이벤트 개요 DOM의 이벤트는 DOM 내의 element, document 개체, window 개체와 관련되어 발생하는 사전 정의된 시점이나 사용자 정의 시점을 말한다. 이 시점이 발생할 때 실행될 기능(핸들러/콜백)

반응형
반응형

docs(KO)
docs(EN)
docs/api(EN)
redux-saga로 비동기처리와 분투하다
Redux-saga: 제너레이터와 이펙트

redux-saga

redux-saga는 애플리케이션의 side effect 를 보다 쉽게 관리하고, 실행하기 쉽고, 테스트하기 쉬우 며, 오류를 더 잘 처리하는 것을 목표로 하는 라이브러리입니다.

Mental model은 saga가 side effect에 대한 책임이 있는 애플리케이션의 별도 스레드와 같다는 것입니다. redux-saga는 redux 미들웨어이기 때문에,이 스레드는 정상적인 redux 작업으로 메인 애플리케이션에서 시작, 일시 중지 및 취소 할 수 있으며 전체 redux 애플리케이션 상태에 액세스 할 수 있으며 redux 작업도 전달(dispatch)할 수 있습니다.

Generators라는 ES6 기능을 사용하여 이러한 비동기 흐름을 쉽게 읽고, 쓰고, 테스트 할 수 있습니다. 이렇게하면 비동기 흐름이 표준 동기 JavaScript 코드처럼 보입니다. (async/await와 비슷하지만 Generator는 우리가 필요로하는 몇 가지 멋진 기능을 더 가지고 있습니다)

비동기 작업을 처리하기 위해 이전에 redux-thunk를 사용했을 수 있습니다. redux thunk와는 달리 콜백 지옥으로 끝나지 않고 비동기 흐름을 쉽게 테스트 할 수 있으며 작업은 순수하게 유지됩니다.

Side effect: data fetching와 같은 비동기식 작업 또는 브라우저 캐시 액세스와 같은 순수하지 않은 동작

Quick Start

프로젝트가 이미 React, Redux로 이루어져있고, redux-saga 를 추가한다고 가정하겠습니다.

폴더 구조는 이렇습니다.

...
/store
  /reducer
  /saga
    |index.js
    |exampleSaga.js
  |index.js
App.jsx

/store/saga/exampleSaga.js

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

// worker Saga: 비동기 증가 태스크를 수행할겁니다.
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

// watcher Saga: 각각의 INCREMENT_ASYNC 에 incrementAsync 태스크를 생성할겁니다.
export function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

/store/saga/index.js

// 모든 Saga들을 한번에 시작하기 위한 단일 entry point 입니다.
export default function* rootSaga() {
  yield all([
    helloSaga(),
    watchIncrementAsync()
  ])
}

/store/index.js

// ...
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = ...
sagaMiddleware.run(rootSaga)

// ...

들어가며 (Generator and Effect)

reference

들어가기에 앞서서 어려운 개념을 먼저 짚어보자.

Generator

앞서 말했듯이 redux-saga 에서는 Generator 문법을 사용한다. 그리고 redux-saga 에서 말하는 saga는 바로 Generator function이다. (Generator가 아니다) saga를 만들어 redux-saga 미들웨어에 등록하면 이 미들웨어가 generator function 으로부터 만들어진 generator를 계속 실행하여 산출된 모든 effect를 실행한다. 즉, saga는 yield 값만을 반환하기만 하고 미들웨어가 이 값(effect)를 받아서 실행하는 역할을 맡는다. saga는 단순히 반환만 하기 때문에 (직접 비동기 처리를 하지 않음) 테스트가 훨씬 용이해진다.

Effect

그럼 Effect는 무엇인가? saga가 반환하는 값이자 미들웨어가 실행할 명령을 담고 있는 자바스크립트 객체라고 생각하면 된다. 이를 통해 saga가 비동기 처리를 하지 않고 미들웨어에게 실행의 책임을 떠넘길 수 있는 것이다. 정리하자면, Saga는 명령을 담고 있는, 이펙트라 부르는 순수한 객체를 yield 할 것이고, 미들웨어는 이런 명령들을 해석해 처리하고, 그 결과를 다시 Saga에 돌려준다. 바로 위 reference 에서 가져온 내용이며 정리가 훨씬 잘되어있으니 꼭 읽어보길 바란다.

Task

하나의 saga가 실행되는 것을 task라고 부른다.

기본 개념

헬퍼 함수 (effect creators)

redux-saga에서는 Task를 만들기 위해 내부 함수를 감싸는 몇몇 helper 함수를 제공한다. (effect creators라고도 부른다.)

functiondescription
takeEvery(action, sagaFn) action이 발생할 때마다 Task(sagaFn)가 실행되게 한다. 여러 개의 Task를 동시에 시작할 수 있다. redux-thunk와 비슷한 기능이다.
takeLast(action, sagaFn) 마지막으로 발생한 하나의 action에만 Task(sagaFn)가 실행되게 한다. 실행중이던 Task는 action이 발생하면 취소되고 새로운 Task가 실행된다.

서술적 이벤트

Saga 로직을 표현하기 위해서 saga는 순수 Javascript object를 yield한다. 이런 object를 effect라고 부른다. effect란 미들웨어에 의해 해석되는 몇몇 정보들을 담고있는 간단한 객체이다. 어떤 기능을 수행하기 위해 미들웨어에 전해지는 명령(스토어에 액션을 dispatch하는 행위나 비동기 함수를 호출하는 등)이라고 볼 수 있다.

effect는 redux-saga/effects 에서 제공하는 함수(effect creator)로 만들어진다. 대표적인 effect creator는 아래와 같다.

functiondescription
select state에서 필요한 데이터를 꺼낸다.
put Action을 dispatch한다.
take Action/이벤트 발생을 기다린다.
call(fn, ...args) Promise의 완료를 기다린다. apply 함수와 동일하다.
fork 다른 Task를 시작한다.
join 다른 Tack의 종료를 기다린다.

3. Advanced Concepts

3.1 Pulling future actions (take)

위에서 살펴봤던 takeEvery 함수를 사용하는 것은 redux-thunk와 유사하다. 사실, takeEvery는 take  fork 함수를 사용한 high-level API에 불과하다.

const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
  while (true) {
    const action = yield take(patternOrChannel)
    yield fork(saga, ...args.concat(action))
  }
})

take는 특정한 액션이 dispatch될 때까지 기다린다. takeEvery의 경우, 실행된 태스크는 그들이 다시 실행될 때에 대한 관리 방법이 없다. take의 경우 액션이 푸시(push)되는 대신, saga 스스로 액션을 풀링(pulling)하기 때문에 특별한 컨트롤 프로우를 수행할 수 있게 한다. 전통적인 액션의 푸시 접근법을 해결하는 것이다.

이 pulling 접근법은 동기적(synchronous) 스타일로 컨트롤 플로우를 표현할 수 있게 한다. 예를 들어 LOGIN, LOGOUT 액션을 이용하여 로그인 플로우를 만들고 싶을 때 takeEvery를 이용하면 두 개의 테스크(saga)를 작성해야 했을 것이다. 하지만 take를 사용해 하나의 태스크로 만들 수 있다.

function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}

3.2 non-blocking calls (fork)

saga는 effect creators 함수를 통해 blocking 하게 작동한다. 이 때문에 발생하는 몇 가지 문제점이 있어 non-blocking을 지원하기 위한 fork effect creator를 제공한다. Task를 fork한다면 그 테스크는 백그라운드에서 시작되고, 호출자는 fork된 테스크가 종료될 때까지 기다리지 않고 플로우를 계속해서 진행한다. yield fork는 task object 를 반환하기 때문에 후에 테스크 취소가 가능하다.

3.3 Running tasks in parallel (all)

병렬 처리가 필요한 경우 all 을 사용한다.

import { all, call } from 'redux-saga/effects'

// correct, effects will get executed in parallel
const [users, repos]  = yield all([
  call(fetch, '/users'),
  call(fetch, '/repos')
])

위와 같이 effectdml 배열을 yield하면, 제너레이터는 모든 effect들이 resolve되거나, 어느 하나라도 reject될 때까지 봉쇄(blocked)된다.

3.4 Starting a race between multiple Effects (race)

여러 task를 병렬로 시작하지만, 그 task를 전부 기다리고 싶지 않을 때가 있다. 이 경우는 race 를 사용한다. race는 첫 번째로 resolve(or reject)된 task가 나오면 나머지 task를 자동으로 취소시킨다.

3.11 Connecting Sagas to external Input/Output

take 는 스토어에 dispatch될 액션이 들어오면 resolve 되었다. 그리고 put은 액션을 인자로 dispatch함으로써 resolve된다. saga가 시작될 때 미들웨어는 자동으로 take/put을 스토어와 연결한다. 이 두 이펙트는 saga의 입력/출력처럼 보일 수 있다.

redux-saga는 리덕스 미들웨어 환경 바깥에서 사가를 실행하고 커스텀 입/출력에 연결할 수 있는 방법을 제공한다.

import { runSaga } from 'redux-saga'

function* saga() { ... }

const myIO = {
  subscribe: ..., // this will be used to resolve take Effects
  dispatch: ...,  // this will be used to resolve put Effects
  getState: ...,  // this will be used to resolve select Effects
}

runSaga(
  saga(),
  myIO
)

3.12 Using Channels

채널은 외부의 이벤트 소스 또는 사가 간의 통신을 위해 해당 이펙트를 일반화한다. 또한 스토어에서 특정 작업을 대기열에 넣을 때도 사용할 수 있다.

이 장에서, 다음 내용을 살펴볼 것이다.

  • yield actionChannel 이펙트를 이용해 스토어의 특정 액션을 버퍼링하는 방법
  • eventChannel 팩토리함수를 사용하여 take 이펙트를 외부 이벤트 소스에 연결하는 방법
  • 일반 channel 팩토리 함수를 이용하여 채널을 만드는 방법과 사가 간의 통신을 위해 take/put 이펙트에 이를 사용하는 방법

actionChannel effect 사용하기

import { take, fork, ... } from 'redux-saga/effects'

function* watchRequests() {
  while (true) {
    const {payload} = yield take('REQUEST')
    yield fork(handleRequest, payload)
  }
}

function* handleRequest(payload) { ... }

위 예제는 전형적인 watch와 fork 패턴이다. 짧은 시간에 많은 액션이 들어온다면 동시에 많은 haneldRequest 테스크가 실행될 것이다. 근데 우리는 이 테스크를 순차적으로 처리하고 싶다. 그래서 우리는 아직 처리되지 않은 액션을 대기열에 집어넣고, 현재 요청을 모두 처리했다면 대기열에서 다음 것을 가져올 것이다. 이를 actionChannel을 사용해 구현할 수 있다.

import { take, actionChannel, call, ... } from 'redux-saga/effects'

function* watchRequests() {
  // 1- Create a channel for request actions
  const requestChan = yield actionChannel('REQUEST')
  while (true) {
    // 2- take from the channel
    const {payload} = yield take(requestChan)
    // 3- Note that we're using a blocking call
    yield call(handleRequest, payload)
  }
}

function* handleRequest(payload) { ... }
  1. actionChannel을 생성한다. take와의 차이점은, saga가 아직 그 액션을 처리할 준비가 되지 않았다면 actionChannel은 들어오는 메시지(액션)을 버퍼링할 수 있다는 것이다.
  2. 스토어에서 특정 액션을 받기 위해 take(pattern) 에 넣을 패턴을 사용한 것처럼, take(channel) 도 가능하다. take는 메시지를 받을 수 있을 때만 saga를 봉쇄할 것이다. 채널 버퍼에 메시지가 저장되어 있을 경우에만 봉쇄되지 않고 진행할 것이다.
  3. saga는 call(handleRequest)가 반환될 때까지 봉쇄를 유지할 것이다. 하지만 봉쇄되어 있는 중에 다른 REQUEST 액션이 dispatch된다면, 그것은 채널의 버퍼에 저장될 것이다. saga가 call(handleRequest)에 의해 재개되고 다음 yield take(requestChan)이 실행될 때, take는 대기열에 저장된 메시지를 resolve할 것이다.

기본적으로 actionChannel은 제한 없이 버퍼링이 가능하나 버퍼 인자를 주어 제한할 수도 있다.

eventChannel 팩토리를 사용해 외부 이벤트에 연결하기

eventChannel(effect creator가 아닌 팩토리 함수)는 리덕스 스토어가 아닌 외부 이벤트를 위한 채널을 생성한다.

이 예제는 일정한 간격마다 채널을 생성한다.

import { eventChannel, END } from 'redux-saga'

function countdown(secs) {
  return eventChannel(emitter => {
      const iv = setInterval(() => {
        secs -= 1
        if (secs > 0) {
          emitter(secs)
        } else {
          // this causes the channel to close
          emitter(END)
        }
      }, 1000);
      // The subscriber must return an unsubscribe function
      return () => {
        clearInterval(iv)
      }
    }
  )
}

eventChannel의 첫 번째 인자는 구독자(subscriber) 함수이다. 구독자의 역할은 외부의 이벤트 소스를 초기화하고 (위의 setInterval 사용), 제공된 emitter를 실행하여 소스에서 채널로 들어오는 모든 이벤트를 라우팅한다. 위의 예제에서 우리는 매 초마다 emitter를 호출한다.

주의: 이벤트 채널을 통해 null 또는 undefined를 전달하지 않도록 해야한다. 숫자를 전달하는 것이 좋지만, 이벤트 채널 데이터를 리덕스 액션처럼 구조화하는 것을 추천한다. number를 { number }로 바꾸는 것과 같이.

emitter(END) 호출에도 주의해야 한다. 우리는 채널 소비자에게 채널이 폐쇄되었다는 것을 알리기 위해 사용한다. 이는 더 이상 다른 메시지가 이 채널을 통해 올 수 없다는 것을 의미한다.

우리의 사가에서 이 채널을 어떻게 쓰는지 보자 (이 예제는 저장소(repository)의 cancellable-counter 예제에서 가져왔습니다.)

import { take, put, call } from 'redux-saga/effects'
import { eventChannel, END } from 'redux-saga'

// creates an event Channel from an interval of seconds
function countdown(seconds) { ... }

export function* saga() {
  const chan = yield call(countdown, value)
  try {    
    while (true) {
      // take(END) will cause the saga to terminate by jumping to the finally block
      let seconds = yield take(chan)
      console.log(`countdown: ${seconds}`)
    }
  } finally {
    console.log('countdown terminated')
  }
}

사가는 take(chan)를 yield하고 있다. 메시지가 채널에 들어가기 전까지 사가는 봉쇄된다. 위의 예제에서, 이는 emitter(secs)를 호출할 때와 일치한다. 우리가 try/finally 구역 내에서 전체 while(true) {...}를 실행하고 있는 것에 주목해보자. countdown의 interval이 종료되면, countdown 함수는 emitter(END)를 호출함으로써 이벤트 채널을 폐쇄한다. 채널을 닫으면 그 채널에서 take에 봉쇄된 모든 사가들을 종료시키는 효과가 있다. 예제에서, 사가를 종료하면 finally 구간으로 점프하게 된다 (finally 구간이 없으면 그냥 종료된다).

구독자는 unsubscribe 함수를 반환한다. 이것은 이벤트 소스가 완료되기 전에 채널 구독을 취소하는 데에 사용된다. 이벤트 채널의 메시지를 소비하는 사가 내에서 이벤트 소스가 완료되기 전에 일찍 나가기를 원한다면 (예로, 사가가 취소됨) chan.close()를 호출해 채널을 폐쇄하고 구독을 취소할 수 있다.

여기 웹 소켓 이벤트를 사가에 전달하여 이벤트 채널을 사용하는 또 다른 예제가 있다. ping이라는 서버 메시지를 기다리고 있고, 조금 뒤에 pong이라는 메시지로 답한다고 가정해보자.

import { take, put, call, apply } from 'redux-saga/effects'
import { eventChannel, delay } from 'redux-saga'
import { createWebSocketConnection } from './socketConnection'

// this function creates an event channel from a given socket
// Setup subscription to incoming `ping` events
function createSocketChannel(socket) {
  // `eventChannel` takes a subscriber function
  // the subscriber function takes an `emit` argument to put messages onto the channel
  return eventChannel(emit => {

    const pingHandler = (event) => {
      // puts event payload into the channel
      // this allows a Saga to take this payload from the returned channel
      emit(event.payload)
    }

    // setup the subscription
    socket.on('ping', pingHandler)

    // the subscriber must return an unsubscribe function
    // this will be invoked when the saga calls `channel.close` method
    const unsubscribe = () => {
      socket.off('ping', pingHandler)
    }

    return unsubscribe
  })
}

// reply with a `pong` message by invoking `socket.emit('pong')`
function* pong(socket) {
  yield call(delay, 5000)
  yield apply(socket, socket.emit, ['pong']) // call `emit` as a method with `socket` as context
}

export function* watchOnPings() {
  const socket = yield call(createWebSocketConnection)
  const socketChannel = yield call(createSocketChannel, socket)

  while (true) {
    const payload = yield take(socketChannel)
    yield put({ type: INCOMING_PONG_PAYLOAD, payload })
    yield fork(pong, socket)
  }
}

주의: eventChannel의 메시지는 기본적으로 버퍼링되지 않는다. 채널의 버퍼링 전략을 지정하려면 eventChannel 팩토리에 버퍼를 인수로 넣어줘야 한다 (예: eventChannel(subscriber, buffer)).

saga간 통신에 채널 사용하기

기본적으로 어떤 소스에도 연결되지 않은 채널을 직접 생성할 수 있다. 그런 다음 채널에 수동으로 put 할 수 있다. 이는 사가 간에 통신을 하기 위해 채널을 사용할 때 유용하다.

3.13 Root Saga Patterns

root Saga는 sagaMiddleware가 실행될 수 있도록 여러 Sagas를 단일 entry point으로 합친다. 초급 튜토리얼에서 루트 사가는 다음과 같이 보일 것입니다.

export default function* rootSaga() {
  yield all([
    helloSaga(),
    watchIncrementAsync()
  ])
  // code after all-effect
}

이것은 루트를 구현하는 몇 가지 방법 중 하나입니다. 여기서 all effect는 배열과 함께 사용되며 sagas는 병렬로 실행됩니다. 다른 루트 구현은 오류 및 더 복잡한 데이터 흐름을 더 잘 처리하는 데 도움이 될 수 있습니다.

Non-blocking fork effects

contributor @slorber가 issue #760에서 몇 가지 다른 일반적인 루트 구현을 언급했습니다. 시작하려면 위 예제와 유사하게 작동하는 인기있는 구현이 하나 있습니다.

export default function* rootSaga() {
  yield fork(saga1)
  yield fork(saga2)
  yield fork(saga3)
  // code after fork-effect
}

세 개의 고유한 yield fork를 사용하면 task descriptor가 세 번 반환됩니다. 앱의 결과 동작은 모든 하위 사가가 동일한 순서로 시작되고 실행된다는 것입니다. 포크가 차단되지 않기 때문에 rootSaga는 자식 무용담이 계속 실행되고 내부 효과에 의해 차단되는 동안 완료 될 수 있습니다.

하나의 큰 모든 effect와 여러 개의 포크 effect의 차이점은 all effect가 blocking이라는 것이다. 따라서 all-effect 이후의 코드는 모든 자식 sagas가 완료 될 때 실행되는 반면, 포크 효과는 차단되지 않으므로 이후 코드 포크 효과는 포크 효과를 산출 한 직후에 실행됩니다. 또 다른 차이점은 포크 효과를 사용할 때 작업 설명자를 얻을 수 있다는 것입니다. 따라서 후속 코드에서 작업 설명자를 통해 분기 된 작업을 취소/결합 할 수 있습니다.

Nesting fork effects in all effect

const [task1, task2, task3] = yield all([ fork(saga1), fork(saga2), fork(saga3) ])

루트 사가를 디자인 할 때 또 다른 인기있는 패턴이 있습니다. 모든 효과의 중첩 포크 효과입니다. 이렇게하면 작업 설명 자의 배열을 얻을 수 있으며 각 포크 효과가 non-blocking이고 동기적으로 작업 설명자를 반환하기 때문에 모든 effect 이후의 코드가 즉시 실행됩니다.

fork 효과는 all 효과에 중첩되지만 항상 기본 forkQueue를 통해 상위 작업에 연결됩니다. 분기 된 작업에서 포착되지 않은 오류는 상위 작업으로 버블링되므로 중단(및 모든 하위 작업)-상위 작업에서 포착 할 수 없습니다.

Avoid nesting fork effects in race effect

// DO NOT DO THIS. The fork effect always win the race immediately.
yield race([
  fork(someSaga),
  take('SOME-ACTION'),
  somePromise,
])

반면에 레이스 효과의 포크 효과는 버그 일 가능성이 높습니다. 위의 코드에서 포크 효과는 차단되지 않기 때문에 항상 즉시 레이스에서 승리합니다.

Keeping the root alive

실제로 rootSaga가 개별 하위 효과 또는 saga의 첫 번째 오류에서 종료되고 전체 앱이 충돌하므로 이러한 구현은 그다지 실용적이지 않습니다! 특히 Ajax 요청은 앱이 요청하는 모든 엔드 포인트의 상태에 따라 앱을 결정합니다.

spawn은 부모와 자식 사가의 연결을 끊는 효과로 부모와 충돌하지 않고 실패 할 수 있습니다. 분명히 이것은 오류가 발생할 때에도 여전히 처리해야하는 개발자로서의 우리의 책임에서 벗어나지 않습니다. 실제로 이로 인해 개발자의 관점에서 특정 실패가 가려지고 향후 문제가 발생할 수 있습니다.

스폰 효과는 React의 Error Boundaries와 유사한 것으로 간주 될 수 있는데, 이는 saga 트리의 특정 수준에서 추가 안전 조치로 사용되어 실패한 기능을 차단하고 전체 앱이 충돌하지 않도록 할 수 있다는 점입니다. 차이점은 React Error Boundaries에 대해 존재하는 componentDidCatch와 같은 특별한 구문이 없다는 것입니다. 여전히 자체 오류 처리 및 복구 코드를 작성해야합니다.

export default function* rootSaga() {
  yield spawn(saga1)
  yield spawn(saga2)
  yield spawn(saga3)
}

이 구현에서는 한 사가가 실패하더라도 rootSaga와 다른 사가가 죽지 않습니다. 그러나 실패한 사가는 앱의 수명 동안 사용할 수 없기 때문에 문제가 될 수도 있습니다.

Keeping everything alive

어떤 경우에는 실패시 sagas를 다시 시작할 수있는 것이 바람직 할 수 있습니다. 장점은 앱과 sagas가 실패한 후에도 계속 작동 할 수 있다는 것입니다 (예 : takeEvery (myActionType)을 생성하는 saga). 그러나 우리는 이것을 모든 사가를 살리기위한 포괄적 인 해결책으로 권장하지 않습니다. 무용담이 정확하고 예측 가능하게 실패하고 오류를 처리 / 기록하는 것이 더 합리적 일 가능성이 높습니다.

예를 들어 @ajwhite는이 시나리오를 사가를 살아있게 유지하면 해결하는 것보다 더 많은 문제가 발생하는 경우를 제공했습니다.

function* sagaThatMayCrash () {
  // wait for something that happens _during app startup_
  yield take(APP_INITIALIZED)

  // assume it dies here
  yield call(doSomethingThatMayCrash)
}

sagaThatMayCrash가 다시 시작되면 다시 시작되고 응용 프로그램이 시작될 때 한 번만 발생하는 작업을 기다립니다. 이 시나리오에서는 다시 시작되지만 복구되지 않습니다.

그러나 시작으로 이익을 얻을 수있는 특정 상황에 대해 user @granmoe는 issue #570에서 다음과 같은 구현을 제안했습니다.

function* rootSaga () {
  const sagas = [
    saga1,
    saga2,
    saga3,
  ];

  yield all(sagas.map(saga =>
    spawn(function* () {
      while (true) {
        try {
          yield call(saga)
          break
        } catch (e) {
          console.log(e)
        }
      }
    }))
  );
}

이 전략은 우리의 sagas를 try 블록의 하위 작업으로 시작하는 생성 된 생성기 (루트 부모에서 분리)에 자식 sagas를 매핑합니다. 우리의 saga는 종료 될 때까지 실행 된 다음 자동으로 다시 시작됩니다. catch 블록은 우리의 사가에서 던지고 종료되었을 수있는 모든 오류를 무해하게 처리합니다.

반응형

+ Recent posts