<근본 시리즈 Redux. 2탄. 설정부터 알아보는 리덕스.>

강민수·2022년 5월 21일
0

근본 시리즈

목록 보기
6/7

저번 회차에서는 분명 리덕스를 왜 사용하는 지 그리고 핵심 개념에는 어떤 것들이 있는 지 살펴봤다. 어떤 기술이든 마찬가지겠지만, 더욱이 이런 상태 관리 라이브러리는 본인이 직접 써봐야만 더 잘 알 수 있다고 생각한다. 필자 역시도, 이번에 다양한 몇 가지의 미니 프로젝트들 속에 실제로 리덕스를 적용해 보면서 이 구조에 대해 확실히 더 잘 알 수 있었다.

자 그러면, 이제 어떤 식으로 돌아가고 코드가 구성되어 지는 지, 한 번 코드를 통해 살펴 봅시다.

1. redux 설치.

자 먼저, 리덕스 역시 라이브러리이기 때문에 설치가 필수적이다.

npm i redux react-redux
혹은
yarn add redux react-redux

이렇게 총 2가지를 설치한다. 먼저, 리덕스만 설치하는 것이 아니냐고 의문이 들 수 있다. 하지만, 추후 살펴보겠지만, 물론 리덕스만 깔아서도 리덕스 본연의 기능들은 사용 가능하다. 하지만, 리액트의 리덕스 관련 유용한 훅스들을 사용하기 위해서는 리액트 리덕스까지 설치해 주는 것이 보통이다.

이렇게, 설치가 끝나면, 다음처럼 패키지 json dependencies에 추가되어 있는 것을 볼 수 있을 것이다.

2. 리덕스 설정.

일단, 설치는 모두 끝났지만, 이제 리덕스를 쓰려면 몇 가지 설정이 필요하다. 천천히 따라해 보면, 쉽지 않게 설정을 완료할 수 있다.

1). store 설정.

앞서 언급했듯이, 리덕스는 단일 스토어를 지향한다. 따라서 하나의 스토어를 가지고, 하위 컴포넌트들에서 해당 스토어의 스테이트 값들을 사용한다. 이를 위해서는 어떤 설정을 해야 할까? 그렇다. 우리가 평소에 프롭스를 내려주는 것처럼 일종의 스토어도 프롭스처럼 내려주는 그런 상속의 개념이 필요하다.

그래서 우리는 아래처럼 모든 컴포넌트들을 다 모아놓은 app컴포넌트의 부모 컴포넌트인 index 컴포넌트에서 '상속'을 만들어 줘야 한다.

먼저, 리덕스에서 크리에이트 스토어라는 함수를 임포트 해 온다. 이 함수는 결국 리덕스의 스토어를 설정한다는 의미다. 그리고 그 안에는 추후 언급하겠지만, 일반적으로 리듀서가 들어간다. 전에 1탄에서 봤듯이 리듀서는 스테이트 값을 변경하는 것들인데, 이걸 스토어와 연결시켜주는 것이다. 참고로 필자는 여기서 리듀서를 모듈화 시켜서 리덕스 모듈 폴더의 index.tsx에서 관리하고 있다. 그 안에는 루트리듀서라는 함수가 있는데, 그 함수 안에 개별적인 리듀서들이 존재하는 구조다. 자세한 부분은 다음 과정에서 코드로 보여 주겠다. 이렇게 스토어 변수에 앞서 언급한 함수들을 임포트 해와서 써 주면 일단, 스토어의 설정은 끝났다.

다음으로 밑에 보이는 컴포넌트 부분의 설정만 해 주면 된다. 다른 이번에는 리액트 리덕스에서 제공하는 프로바이더라는 공급자 컴포넌트가 있다. 이걸로 감싸주고 프롭스를 내리는 것처럼 store= {store}를 적어주면 끝이다. 이제 모든 컴포넌트들은 프로바이더가 가지고 있는 스토어의 스테이트 값을 참조하거나 루트 리듀서에 속한 리듀서들을 공유하게 된다.

2). Reducer의 설정.

다음은, 앞서 언급한 리듀서의 설정 부분이다. 물론 이런 설정 부분 혹은 디렉토리 구분은 개발자마다 전부 다르기에, 정확한 정답은 없다. 하지만, 보통은 아래 사진처럼 모듈화를 시켜 관리해 주는 것이 관리하기 용이하다고 알려져 있어 이렇게 필자는 사용한다.

1. index

먼저, 아까 언급한 모든 리듀서들을 모아 놓은, index 모듈이다. 여기 선언해 놓은 루트 리듀서가 바로 앞선 store에 쓰인 루트 리듀서가 바로 이 루트 리듀서다. 콤바인 리듀서라는 함수를 리덕스 패키지에서 불러오고, 그 안에 개별적인 리듀서 들을 담으면 된다. 지금은 비록 필자가 단일 리듀서만 기재해 놓았지만, 이외에도 다양한 리듀서들을 추가해서 기재해 주면 된다.

참고로 타입스크립트를 사용중이라면, 저렇게 루트 스테이트의 타입을 루트 리듀서로 지정해서 사용해 줘서 타입 지정을 해줘야 한다.

2. 개별 리듀서 설정.

이제는 개별 리듀서를 기재해 줄 시간이다. 사실 이 리듀서 안에는 액션을 별도로 분리할 수도 있다. 하지만, 이 코드들이 꼭 분리되어있을 필요는 없다. 이 코드들이 서로 다른 디렉터리에, 그리고 서로 다른 파일에 분리가 되어있으면 개발을 하는데 꽤나 불편하다. 그래서 우리는 리듀서와 액션 관련 코드들을 하나의 파일에 몰아서 작성해보도록 한다. 이를 Ducks 패턴이라고 부른다. 리덕스 관련 코드를 분리하는 방식은 정해져있는 방식이 없으므로 나중에는 여러분이 자유롭게 분리해도 된다.

1) action 타입 선언.

먼저, 우리는 어떤 액션을 취할 것인지 액션에 일종의 명칭을 설정해 줘야 한다.

필자는 퀴즈에 대한 정답과 오답 그리고, 완료, 리셋 등의 4가지 타입으로 설정을 해 줬다.

2) 액션 생성 함수 설정.

다음은 바로 어떤 액션을 취할 것인지를 결정하는 액션 생성 함수를 설정해 줘야 한다.

다음과 같이 필자가 미리 설정해 둔 타입에 따라, 액션을 생성해 준다. 이 함수에는 액션에서 어떤 인자를 받아올 지 결정할 수 있으며, 내부에는 이 액션이 어떤 타입을 따르고 있는 지(생략 불가) 적어준다. 그리고 추가적으로 흔히 payload라고 불리는 것으로(물론 생략도 가능함) 액션에서 받아온 값을 적어줄 수 있다.

3) 액션 객체들에 대한 타입 설정.

다음은 타입스크립트로 작성한다면 필수적인 타입 지정이다. 이 카운터 액션에 대한 타입 지정은 바로 후술할 리듀서에서 인자로 들어갈 액션 타입을 결정해 주는 것이라고 생각하면 된다.

4) 리듀서의 초기값 및 타입 설정.

type Quiz = {
  index: number
  text: string // 문제
  answer: string // 정답
  selections: string[] // 보기 목록 (정답 포함), 2지 선다
}

type QuizResult = {
  index: number
  text: string // 문제
  answer: string // 정답
  selected: string // 선택한 답
  isCorrect: boolean // 정답여부
}

type State = {
  isCompleted: boolean // computed
  correctCount: number // computed
  inCorrectCount: number // computed
  currentIndex: number // computed
  payload: number
  isCorrect: boolean
  selected: string
  quizList: Quiz[]
  quizResults: QuizResult[]
}
const initialState: State = {
  isCompleted: false,
  isCorrect: false,
  selected: '',
  correctCount: 0,
  inCorrectCount: 0,
  currentIndex: 0,
  payload: 1,
  quizList: quizData,
  quizResults: quizResultData
}

다음과 같이 이제 우리가 리듀서에서 사용할 스테이트 값들의 초기 값으로 initialState로 지정해 준다. 그리고 그 값이 어떤 타입을 가지는 지 다시 State로 설정해 준다. 여기서는 별도로 quizList와 quizResults를 각각 외부 데이터로 받아온 것으로 보면 되겠다. 그 값들이 배열 형태이므로 다음과 같이 배열 안의 구성값들 역시 지정해 줬다.

5) 리듀서 작성.

이제 드디어 고대하던 리듀서의 작성이다. 리듀서에서는 초기 값과 액션 타입에 따른 변경 값을 가지고 상태 값을 리턴하는 구조라고 생각하면 된다.

구조의 예시는 다음과 같다. 먼저, 리듀서 함수의 이름을 지정한다. 그리고 인자 값으로는 초기 값, 액션을 받는다. 그리고 이제, 액션의 타입을 케이스로 나눠서 별도로 타입마다 다른 결과 값을 도출시켜 줘야 한다.


export default function quizSessionReducer(
  state: State = initialState,
  action: CounterAction
): State {
  // TODO
  // 선택한 선지에 따라
  // state 값이 변경되어야 함.
  // 예를 들어, 퀴즈 결과가 생성되고
  // 맞은 혹은 틀린 개수가 업데이트 되고,
  // 다음 퀴즈로 넘어가야 함.
  switch (action.type) {
    case CORRECT:
      return {
        ...state,
        currentIndex: state.currentIndex + 1,
        correctCount: state.correctCount + 1,
        isCorrect: (state.quizResults[state.currentIndex].isCorrect = true),
        selected: (state.quizResults[state.currentIndex].selected = action.selected),
      }
    case INCORRECT:
      return {
        ...state,
        currentIndex: state.currentIndex + 1,
        inCorrectCount: state.inCorrectCount + 1,
        isCorrect: (state.quizResults[state.currentIndex].isCorrect = false),
        selected: (state.quizResults[state.currentIndex].selected = action.selected)
      }
    case FINAL:
      return {
        ...state,
        isCompleted: true
      }
    case RESET:
      return {
        ...state,
        currentIndex: 0,
        inCorrectCount: 0,
        correctCount: 0,
        isCompleted: false
      }
    default:
      return state
  }
}

필자의 경우, 앞서 언급했듯이, 퀴즈에 따라 선택한 값에 따라 다른 액션을 취해야 하기 때문에, 이후에 적용하는 방법은 다시 살펴보도록 하겠다.

여기서 주목할 점은 바로, 리턴 안에, ...state라는 부분이다. 이 부분은 바로 리덕스가 중요시 하는 상태 값의 불변성을 유지하기 위한 하나의 작업이다. 물론 이후에 immer 라이브러리를 쓰던가 혹은 리덕스 툴킷을 사용하면 이렇게 스프레드 문법의 작성이 필요없다. 하지만, 일단 기본적으로는 리덕스 초창기에는 저렇게 상태 값의 불변성 유지를 위해 복사된 값을 사용해야 하기때문에 스프레드 문법으로 작성해 줘야만 했다.

마지막으로 default에는 아무런 액션 타입이 지정되지 않은 값에는 기본 스테이트로 지정해 줬다. 즉 초기값 상태인 것이다.

이런 식으로 개별 리듀서 설정까지 마친다면, 리덕스에 대한 사용이 가능하다. 셋팅까지는 마쳤지만, 이제 본격적으로 리액트에서 어떻게 사용할 것인지는 직접 실제 사례를 통해 다시 한 번 3탄에서 살펴보도록 하겠다.

profile
개발도 예능처럼 재미지게~

0개의 댓글