2019-06-05 TIL

2019-06-05

어제에 이어서 Free Code Camp의 Redux 챌린지

Redux

Redux: Handle an Action in the Store

action이 만들어지고 dispatch되면, 리덕스 스토어는 그 action에 대해 어떻게 반응해야 할지 알 필요가 있습니다. 이것은 reducer 함수가 할 일입니다. 리덕스의 reducer는 action에 반응해 일어나는 스테이트 변경을 책임집니다. reducerstateaction을 인자로 받으며, 항상 새로운 state를 리턴합니다. 이것이 reducer의 유일한 역할임을 아는것이 중요합니다. reducer는 side effect가 없습니다 — reducer는 절대 API 엔드포인트를 호출하지 않으며 어떠한 숨겨진 예기치 않은 일(surprises)을 벌이지 않습니다. reducer는 단순히 state와 action을 받아 새 state를 리턴하는 순수 함수(pure function)입니다.

리덕스의 또다른 핵심 원칙은 state가 읽기 전용이라는 것 입니다. 달리 말하면, reducer함수는 반드시 항상 state의 새로운 사본을 리턴해야 하며 절대 state를 직접적으로 변경하지 않습니다. 리덕스는 스테이트의 불변성(immutability)를 강제하지 않습니다만, 여러분은 reducer 함수 코드 안에서 불변성을 강제할 책임이 있습니다. 뒤에 다룰 챌린지에서 연습하게 될 것입니다.


옆에 있는 코드 에디터에 이전 예제와 더불어 reducer함수의 기본 형태를 준비해 두었습니다. reducer함수의 body를 채워넣으세요. 'LOGIN' 타입의 액션을 받으면 login속성이 true로 설정된 스테이트 오브젝트를 리턴하게 만드세요. 그렇지 않다면 현재 스테이트를 반환하세요. 현재 state와 디스패치된 acton이 reducer에 전달되어 acton.type으로 액션의 타입에 직접 액세스할 수 있다는 점에 주목하세요.

const defaultState = {
  login: false
};

const reducer = (state = defaultState, action) => {
  // change code below this line
  if (action.type === 'LOGIN') {
    return { login: true };
  } else {
    return state;
  }
  // change code above this line
};

const store = Redux.createStore(reducer);

const loginAction = () => {
  return {
    type: 'LOGIN'
  }
};

Redux: Use a Switch Statement to Handle Multiple Actions

리덕스 스토어가 여러 action type을 어떻게 처리할 지 정해줄 수 있습니다. 여러분이 사용자 인증 정보를 리덕스 스토어 안에서 관리한다고 해봅시다. 여러분은 사용자가 로그인 했을때와 로그아웃했을 때를 표현하는 스테이트가 필요할 것입니다. authenticated라는 속성을 가진 단 하나의 스테이트 오브젝트로 표현할 수 있습니다. 또한 사용자 로그인과 로그아웃에 해당하는 액션을 생성하는 action creator와 action 오브젝트가 필요합니다.


옆에 있는 코드 에디터에 store, action, action creator를 준비해 두었습니다. 여러 인증 액션들을 다루는 reducer 함수를 작성하세요. 서로 다른 액션 이벤트들에 반응할 수 있도록 reducer 안에 switch문을 사용하세요. 이것은 리덕스 reducer를 작성할 때 표준적으로 사용하는 패턴입니다. switch문은 action.type에 따라 switch해야 하며 적절한 인증 스테이트를 리턴해야 합니다.

Note: 현 시점에서는 스테이트의 불변성에 대해 걱정하지 마세요. 작고 간단한 예제니까요. 각 액션에 대해 새 오브젝트를 리턴할 수 있습니다 — 예를 들어, { authenticated: true } 처럼요. 또한, switch문에 현재 스테이트를 리턴하는 default 케이스 작성을 잊지 마세요. 여러분의 앱이 여러 리듀서를 갖고있다면 action dispatch가 생길 때 마다 모든 리듀서가 실행되는데, 발생한 action과 관련되지 않았어도 리듀서는 실행됩니다. 이런 경우에, 현재 스테이트를 리턴해야합니다.

const defaultState = {
  authenticated: false
};

const authReducer = (state = defaultState, action) => {
  // change code below this line
  switch (action.type) {
    case 'LOGIN': return { authenticated: true };
    case 'LOGOUT': return { authenticated: false };
    default: return state;
  }
  // change code above this line
};

const store = Redux.createStore(authReducer);

const loginUser = () => {
  return {
    type: 'LOGIN'
  }
};

const logoutUser = () => {
  return {
    type: 'LOGOUT'
  }
};

Redux: Use const for Action Types

리덕스를 사용하는 일반적인 용법은 action 타입들을 읽기전용 상수(const 변수)로 할당하고 사용처에서 상수를 참조하는 것 입니다. action type을 작성했던 코드를 const 선언으로 리팩터링할 수 있습니다.


LOGINLOGOUTconst 값으로 선언하고 문자열 LOGIN''LOGOUT'을 각각 할당하세요. 그리고, authReducer()와 action creator들이 스트링값 대신 이 상수들을 참조하도록 수정하세요.

Note: 상수는 모두 대문자로 적는것이 일반적인 관례(convention)이고, 리덕스에서도 일반적입니다.

// change code below this line
const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';
// change code above this line

const defaultState = {
  authenticated: false
};

const authReducer = (state = defaultState, action) => {
  switch (action.type) {
    case LOGIN:
      return {
        authenticated: true
      }
    case LOGOUT:
      return {
        authenticated: false
      }
    default:
      return state;
  }
};

const store = Redux.createStore(authReducer);

const loginUser = () => {
  return {
    type: LOGIN
  }
};

const logoutUser = () => {
  return {
    type: LOGOUT
  }
};

Redux: Register a Store Listener

리덕스 스토어 오브젝트에 접근할 수 있는 또다른 방법은 store.subscribe()입니다. 이 메서드를 이용해 리스너 함수가 리덕스 스토어를 구독할 수 있으며, 해당 store에 action이 dispatch될 때 마다 리스너 함수가 호출됩니다. 이 메서드의 한가지 간단한 사용 예제는 action을 받고 store가 업데이트 될 때 마다 단순히 메시지를 기록(log)하도록 함수를 store에 구독시키는 것 입니다.


스토어가 action을 받을 때 마다 전역변수 count를 증가시키는 콜백 함수를 작성하고, 이 함수를 store.subscribe() 메서드에 인자로 전달하세요. 옆에 있는 코드 에디터를 보면 store.dispatch()가 연달아 세 번 호출되고 매번 action 오브젝트를 직접 전달하고 있습니다. 업데이트가 일어나는 것을 확인할 수 있게 dispatch 사이의 콘솔 출력을 지켜보세요.

const ADD = 'ADD';

const reducer = (state = 0, action) => {
  switch(action.type) {
    case ADD:
      return state + 1;
    default:
      return state;
  }
};

const store = Redux.createStore(reducer);

// global count variable:
let count = 0;

// change code below this line
const listener = () => {
  count += 1;
}
store.subscribe(listener);  // 이제 store가 업데이트 될 때 마다 listener 함수를 호출합니다.
// change code above this line

store.dispatch({type: ADD});
console.log(count);        // 1
store.dispatch({type: ADD});
console.log(count);        // 2
store.dispatch({type: ADD});
console.log(count);        // 3

Redux: Combine Multiple Reducers

여러분의 앱의 스테이트가 점점 복잡해지기 시작할 때, 스테이트를 여러 조각으로 분리하고 싶은 유혹이 있을겁니다. 대신에, 리덕스의 첫번째 원칙을 기억하세요: 모든 앱 스테이트는 store 안의 하나의 스테이트 오브젝트에 담긴다. 따라서, 리덕스는 복잡한 스테이트 모델에 대한 솔루션으로 리듀서 합성(reducer composition)을 제공합니다. 스테이트의 서로 다른 조각들을 다루기 위해 여러 리듀서들을 정의하고, 이 리듀서들을 모두 합쳐 하나의 root reducer로 만듭니다. 이 root reducer는 createStore()메서드에 인자로 전달됩니다.

여러 리듀서들을 하나로 합치기 위해, 리덕스는 combineReducers()메서드를 제공합니다. 이 메서드는 인자로 한 오브젝트를 받는데, 특정 리듀서 함수에 연결되는 키를 속성으로써 정의한 것입니다. 여러분이 키로 정한 이름은 리덕스에 의해 스테이트의 연관된 조각의 이름으로써 사용됩니다.

보통은, 어플리케이션 스테이트의 각 조각이 서로 구별되거나 어떤식으로 유니크할 때, 조각마다 리듀서를 만드는 것은 좋은 습관입니다. 예를 들면, 사용자 인증을 포함하는 노트 앱에서, 한 리듀서는 인증을 다루고, 다른 한 리듀서는 유저가 제출하는 텍스트와 노트를 다룰 수 있습니다. 이런 어플리케이션에 대해, 우리는 다음과 같이 combineReducer() 메서드를 작성할 수 있습니다 :

const rootReducer = Redux.combineReducers({
  auth: authenticationReducer,
  notes: notesReducer
});

이제, notes라는 키는 우리의 노트에 연관된 모든 상태를 담게 되며 notesReducer에 의해 처리될 것입니다. 이것은 더욱 복잡한 어플리케이션 상태를 관리하기 위해 여러 리듀서들을 합성(compose)하는 방법입니다. 이 예제에서, 리덕스 스토어에 담긴 스테이트는 auth 속성과 notes속성을 담는 하나의 오브젝트입니다.


옆에 있는 코드 에디터에 counterReducer()authReducer()함수, 리덕스 스토어가 준비돼있습니다. Redux.combineReducers()메서드를 사용해 rootReducer()함수의 작성을 완료하세요. counterReducercount속성에, authReducerauth속성에 할당하세요.

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

const counterReducer = (state = 0, action) => {
  switch(action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};

const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';

const authReducer = (state = {authenticated: false}, action) => {
  switch(action.type) {
    case LOGIN:
      return {
        authenticated: true
      }
    case LOGOUT:
      return {
        authenticated: false
      }
    default:
      return state;
  }
};

const rootReducer = Redux.combineReducers({ // define the root reducer here
  count: counterReducer,
  auth: authReducer
}) 

const store = Redux.createStore(rootReducer);

Redux: Send Action Data to the Store

지금까지 여러분은 리덕스 스토어에 액션을 디스패치하는 방법을 배웠지만, 지금까지의 액션들은 type 외 어떤 정보도 담고있지 않았습니다. 물론 데이터를 액션과 함께 보낼 수 있습니다. 사실, 액션은 보통 유저의 UI 상호작용으로부터 발생하고 동시에 어떤 데이터를 발생시키는 경향이 있기 때문에 아주 보편적으로 행해집니다. 리덕스 스토어는 종종 이 데이터에 대해 알 필요가 있습니다.


옆에 있는 코드 에디터에 기본적인 notesReducer()addNoteText()라는 action creator가 정의되어있습니다. action 오브젝트를 리턴하도록 addNoteText()의 본문 작성을 완료하세요. 오브젝트는 반드시 ADD_NOTE를 값으로 갖는 type속성을 포함해야 하며, text 속성도 포함해야 하는데, action creator에 인자로 전달된 note를 값으로 설정하세요. 이 action creator를 호출할 때, 특정 노트 정보를 인자로 전달해야 합니다.

다음으로, notesReducer()switch문의 작성을 완료하세요. addNoteText() 액션을 처리할 수 있는 케이스를 추가해야 합니다. ADD_NOTE타입의 액션이 있을 때 마다 이 케이스가 촉발되어야 하며 들어오는 action에 대해 text속성을 새 state로서 리턴해야 합니다.

코드의 마지막 부분에 액션이 디스패치되었습니다. 작성을 완료하시면 코드를 실행하고 콘솔을 지켜보세요. 이것이 스토어에 액션한정적(action-specific) 데이터를 보내고 스토어 state를 업데이트할 때 사용하는 전부입니다.

const ADD_NOTE = 'ADD_NOTE';

const notesReducer = (state = 'Initial State', action) => {
  switch(action.type) {
    // change code below this line
    case ADD_NOTE:
      return action.text;
    // change code above this line
    default:
      return state;
  }
};

const addNoteText = (note) => {
  // change code below this line
  return { type: ADD_NOTE, text: note };
  // change code above this line
};

const store = Redux.createStore(notesReducer);

console.log(store.getState());      // Initial State
store.dispatch(addNoteText('Hello!'));
console.log(store.getState());      // Hello!

Minchang Kim
Minchang Kim
웹/앱 개발자 김민창입니다! 좋은 하루 되세요!