[React] 리덕스 설치와 필요성 소개, 리덕스의 원리와 불변성, 리덕스 실제로 구현하기

Yuri Lee·2022년 5월 22일
0

이 글은 인프런 제로초님의 '[리뉴얼] React로 NodeBird SNS 만들기' 를 바탕으로 정리한 내용입니다.

리덕스 설치와 필요성 소개

npm trends 에서 redux, mobx 를 비교해볼 수 있다. redux는 초보에게 좋고, mobx는 어느정도 리액트 생태계를 이해하는 사람들에게 좋다. next.js 리덕스를 붙이는데 꽤나 복잡한데 가볍게 해주는 쉽게 해주는 라이브러리가 있다. redux-wrapper

redux-wrapper, redux 설치하기

npm i next-redux-wrapper@6

→ 6버전으로 고정해주기

npm i redux

예전 버전의 redux-next에서는 provider로 감싸주는 부분이 있었다. 하지만 6버전으로 오면서 알아서 provider로 감싸주기 때문에 여기에서는 그 과정을 추가하지 않아도 된다.

import React from 'react'
import 'antd/dist/antd.css'
import PropTypes from 'prop-types'
import Head from 'next/head'

import wrapper from '../ store/configureStore'

const Nodebird = ({ Component }) => {
    return (
        <Provider={store}>
            <Head>
                <meta charSet='utf-8' />
                <title>NodeBird</title>
            </Head>
            <Component />
        </Provider>

    )
}

Nodebird.protoTypes = {
    Component: PropTypes.elementType.isRequired,
}

export default wrapper.withRedux(Nodebird);

리덕스를 사용하는 이유

로그인 폼, 회원가입 페이지 등등에 공통적인 정보가 있다. 로그인한 사람 여부, 닉네임 정보, 로그인 여부 등등. 하지만 각각의 컴포넌트가 분리되어 있다. 부모 컴포넌트에서 데이터를 받아서 각각 자식 컴포넌트에게 보내줘야 하는데, 그런 과정들이 매우 귀찮다. 따라서 중앙에서 관리해서 데이터를 전달해주는 중앙 데이터 저장소인 리덕스를 사용하는 것이다.

React의 context API 도 그런 역할을 해준다. 데이터들을 중앙 저장소에서 하나로 모아두고, 컴포넌트가 필요로 할때 그 데이터 전체를 가져다 사용하거나 부분적으로 가져다 사용한다. 중앙저장 데이터 저장소는 하나정도는 만들어 놓는게 좋다.

→ react 의 context api, redux, mobx, apollo

리덕스, 어떻게 고를까?

규모가 있는 프로젝트라면 저장소 최소 1개는 필요하다. 데이터는 중앙에서 한번에 관리하는 게 편하다. 제일 많이 쓰이는 것은 리덕스이고, 초보를 탈출하고 싶다면 mobx이다. 앱 크기가 크지 않을 경우 context API 사용해도 좋다.

  • redux 는 사용할 경우 에러가 나도 잘 해결이 된다. 추적이 잘 되기 때문이다. 앱이 안정적이게 된다. 하지만 단점은 코드량이 많아진다.
  • mobx 는 코드량이 적어지지만, 추적이 약간 어렵다.

개발자 성향, 팀 성향에 따라서 무엇을 사용할지는 달라진다.

context api 랑 redux/mobx 의 차이점은?

비동기를 지원하기 쉽냐, 어렵냐이다. 서버에서 데이터를 받아오는 것은 항상 비동기이다. 가끔씩 서버가 고장났을 수도 있고 에러가 생겼을 수도 있다. 비동기를 다룰 때는 항상 실패에 대응해야 한다.

  1. 데이터 요청
  2. 데이터 성공해서 받기
  3. 실패하는 것

이것을 컨텍스트 API에서 구현하려면 다음의 3단계를 직접 다 구현해줘야 한다. 보통 단점이 uesEffect에다가 많이 요청을 보낸다.

useEffect(() => {
	 axios.set("/data')
		.then() => { 
			setState(data)
	})
		.catch(() => {
			setError(error);
	}

컴포넌트 안에 이러한 로직들이 많이 들어갈 경우 의도치 않은 코드 중복이 발생할 수도 있다. 그래서 컴포넌트 안에서는 데이터를 불러오는 것을 최대한 피하는 편이다. reudx 나 mobx 에 맡기는 게 좋다. 화면을 비즈니스 로직이랑 분리하기!

데이터는 부모로부터 받거나 hooks 으로부터 받고, 데이터는 중앙에서 관리하면 좋다. 대신 앱이 커지면 데이터도 엄청 커진다. 적절히 쪼개주는 작업이 필요하다. redux는 reducer 들로 여러 개로 쪼갤 수 있다. 어느정도 쪼갤 수 있는 시스템을 갖춰놓는 것을 추천한다.

리덕스의 원리와 불변성

리덕스는 리듀스에서 이름을 따온 것이다.

다음의 데이터가 있다고 가정하자.

{
	name: 'zerocho',
	age: 27,
	password: 'babo',
}

리덕스에서는 데이터를 바꾸려면 액션이라는 것을 만들어줘야 한다. 예를 들어 다음의 액션이 있다고 하자.

{
	type: 'CHANGE_NICHKNAME',
	data: 'boogicho'
}

type이 액션의 이름이다. 이 action을 dispatch 한다. name을 가져다가 사용하고 있는 컴포넌트들이 다 zerocho에서 boogicho로 바뀐다. reducer는 js가 마법을 부리는 것이 아니다. action을 dispatch 한다고 해서 알아서 네임을 boogicho로 바꾸는 게 아니다. js는 CHANGE_NICHKNAME 이 뭔지 이해를 못한다. 따라서 reducer에서는 직접 어떻게 바꿔야할지 일일히 적어줘야 한다.

reducer

switch(action.type) {
	case 'CHANGE_NICHKNAME :
		return {
			...state,
			name: action.datae,
		}
}

처음에 앱에 대한 전체 상태를 하나의 객체로 표현해놓고, 바꾸고 싶을 때마다 액션을 추가한다. 액션을 하나 만들어서 dispatch 하면 된다. 그러면 나이가 바뀐다. 자동으로 바뀌는 것은 아니고 reducer 에서 구현해줘야 한다. 어떻게 보면 마법 같은 건 하나도 없다.

{
	type: 'CHANGE_NICKNAME',
	data: 'boogicho'
}
switch(action.type) {
	case 'CHANGE_NICHKNAME :
		return {
			...state,
			name: action.datae,
		}
	case 'CHANGE_AGE :
	return {
		...state,
		age: action.datae,
	}
}

이처럼 직접 구현하는 것이다. reducer 에 적어준 방법에 따라 state 값이 달라진다. 코드량이 매우 많아진다. 비밀번호를 바꾸고 싶다/닉네임,나이, 비밀번호를 동시에 바꿔줘야 할 때도 다 추가해줘야 한다.

굳이 귀찮게 왜?

액션 하나가 리덕스에 기록이 된다. 내역들이 다 추적된다. 액션, 리듀서를 실수로 만들었을 때 포착하기 쉽다. 액션들이 정렬되어서 출력되기 때문에 데이터가 잘못되어서 에러가 나는 부분을 쉽게 찾을 수 있다. hisotry 가 있으면 거꾸로 거슬러 올라갈 수 있다. 타임머신을 타고 올라가는 것과 비슷하다. 데이터를 뒤로 돌렸다가 감았다가 등의 테스트 하기 좋다.

단점

swith 문이 엄청 길어진다. swith 문이나 state 문을 쪼갤 수 있긴 하지만, 길어지는 게 단점이라면 단점이다.

case 문 안에서 리턴을 왜 이렇게 적어줄까?

불변성 (Immutablility) 때문이다.

{ } === { } // false : 객체를 새로 만들어줄 때는 항상 false 가 나온다. 
const a = {};
const b = a;
a === b // true
return {
  ...state,
  name: action.datae,
}

중괄호이므로 객체를 새로 만들어준 것이다. 항상 false가 나온다. 항상 다른 객체를 리턴하는 것이다. 내가 바꾸고 싶은 것만 바꿔주는 것이다. 내가 바꾸고 싶은 것만 바꾸고, 닉네임 부분만 바꿔주고, 객체를 새로 만들어서 보내준다.

Q. 왜 객체를 새로 만들까?

const prev = { name: 'zerocho'}
const next = { name: 'boogicho' }

이전 기록, 다음 기록을 다 남길 수 있기 때문이다. 이 이유로 새롭게 객체를 만들어주는 것이다.

const next = prev;
next.name = 'boogicho'
prev.name // boogicho

만약 새로운 객체로 안만들고, 직접 바꿔버리면 히스토리가 없어진다. 리덕스를 사용하는 주요 목적 중 하나가 히스토리 관리를 하는 것이다. 하지만 참조 관계일 경우 기록이 다 사라진다.

return {
  ...state,
  name: action.datae,
}

그래서 다음과 같이 항상 새로운 객체를 만들어준다.

Q. 그럴꺼면 차라리 이렇게? ... 하지말고 전체 적어도 되는 거 아닌가?

{
	...state,
	name: action.data,
	age: 27,
	password: 'babo'
}

타자가 길어지는 이유도 있지만, 메모리도 아끼기 위해서이다.

{
	...state,
	name: action.data
}
{
	name: 'zerocho',
	age: 27,
	password: 'babo',
	posts: [{}, {}, {}]
}

... 를 사용하면 참조가 된다. 유지해도 되는 것은 참조 관계 로 해주고, 바뀌는 애들만 바꿔주는 것이다. 그게 목적이다. 메모리를 아낄 수 있다. 객체를 새로 만들면 영원히 있는게 아니다. 정리가 된다.

개발 모드에서는 히스토리가 다 남아있는데, 배포모드로 바꾸면 히스토리를 중간중간마다 바꿔준다. 매모리 정리를 해준다. 배포 버전일 때는 메모리 문제가 일어나지 않는다. 비구조화할당(Destructuring Assignment) 을 하면 새로 생성되는 게 아니다.

+) 예제 하나 더 !

얕은복사/ 깊은 복사

const nest = { b: 'c'};
const prev = { a: nest };

const next = { ...prev}
 
prev.a === next.a // 같음 
prev === next // false

.. 오브젝트 스프레드 하면 객체들은 이전 상태와 다음 상태, 참조를 유지하지만 prev와 next 자체는 다르다. prev와 next 자체는 다르다. 새로운 객체를 만들어준 것이기 때문이다. 그 안에 들어있는 a는 참조를 유지하기 때문에 메모리를 아낄 수 있다. 일일히 다 적어주면...

{  a : 'b'} === {a: 'b'} // false

참조를 적절하게 유지할 때는 nest 된 것들은 유지하고, (next 새로 만들어줘야 할때는 또 새로 만들어주는 게 좋다.

Q. 배열 같은 경우에는 이런식으로 하면 그냥 뒤에 새로운 값이 추가가 되는걸로 아는데

return [

...state,

{ name: action.data },

]

객체의 경우에는 이렇게하면 중복되는 name 속성은 뒤에걸로 덮어 씌워지는건가요?

return {

...state,

name: action.data,

}

→ O, 앞에 name이 있다면 뒤의 name으로 덮어씌어짐.

리덕스 실제로 구현하기

reducers > index.js

reducer는 (이전상태, 액션) 통해서 다음 상태로 만들어주는 함수이다. (Switch문이 들어감) reducer 는 차원 축소의 뜻을 가지고 있는데, 거기서 리덕스라는 개념이 나온 것이다.

store란 state와 reducer를 포함한 것.

const iinitialState = {
	name: '철수',
	age: 24,
	password: 'babo',
}

const rootReducer = (state = iinitialState, action) => {
    switch (action.type) {
        case 'CNAHGE_NICKNAME':
            state.name = '영희';
            break;
   }   
}
 
export default rootReducer;

→ 이렇게 직접 바꾸면 참조관계가 유지되어서 히스토리가 남지 않는다. 따라서 X

const rootReducer = (state = iinitialState, action) => {
    switch (action.type) {
        case 'CNAHGE_NICKNAME':
            return {
                ...state,
                name: action.data,
            }
   }   
}

→ 다음의 형식을 사용해야 함.
→ 기본 스테이트가 있고, 이 스테이트를 바꾸고 싶을 때 기본적으로 액션을 만들어서, 나중에 그 액션을 디스패치할 수 있다. 디스패치하는 순간 type, data가 리듀서로 전달된다.

const configureStore = () => {
    const store = createStore(reducer);
    store.dispatch({
        type: 'CHANGE_NICKNAME',
        data: 'yurilee2'
    })
    return store;
};

(이전상태, 액션) => 다음 상태

액션에 따라서 데이터를 바꿔서 다음 상태를 리턴해준다. 리듀서는 축소라는 의미가 있다. 이전 상태 액션, 두개를 받아서 하나를 축소한다. 데이터를 바꾸고 싶다면 액션을 만들어주면 된다. 여기서 여러개의 기법이 있다. 만약 changeNickname을 여러개로 하고 싶다면?

const changeNickname = {
    type: 'CNAHGE_NICKNAME',
    data: '철수2'
}

const changeNickname = {
    type: 'CNAHGE_NICKNAME',
    data: '철수3'
}

const changeNickname = {
    type: 'CNAHGE_NICKNAME',
    data: '철수4'
}

같은 타입의 액션인데 매번 만들어줘야 한다. 이건 너무 비효율적이다. 이럴 때는 함수를 만들어준다. 액션을 만들어주는 함수를 만드는 것이다. 매번 액션을 써주는 게 아니다. 데이터 부분은 매번 바뀔 수 있기 때문이다. (사용자가 닉네임을 어떻게 바꿀지 모름)

동적으로 액션을 생성해주는 액션 크리에이터를 만든다.

// action creator
const changeNickname = (data) => {
    return {
        type: 'CHANGE_NICHNAME',
        data
    }
}
changeNickname('철수2')
// {
//     type: 'CNAHGE_NICKNAME',
//     data: 'yurilee2'
// }
store.dispatch(changeNickname('철수3'))

→ 어떤 액션이든지 그 자리에서 즉흥적으로 만들어서 디스패치해줄 수 있다. 액션은 원래는 객체라서 값을 수정을 못하는데, 사실상 모든 케이스 대응은 불가능하기 때문에 액션 크리이에터를 즉흥적으로 만들어서 그때 그때 만들어서 디스패치를 하면 CNAHGE_NICKNAME 에 action 가 쏙 들어갈 것이다.

그 다음에는 비동기 액션 크리에이터가 나온다 (리액트 사가)

액션 하나 만들어서 디스패치하면 리듀서에 따라 다음 상태가 나오고, 알아서 이전 상태와 다음 상태가 바뀌었다는 게 확인되면 알아서 연결된 컴포넌트에게 데이터가 뿌려진다.

const rootReducer = (state = iinitialState, action) => {
    switch (action.type) {
        case 'LOG_IN':
            return {
                ...state,
                user: {
                    ...state.user,
                    isLoggedIn:true,
                }
            }
   }   
}

iinitialState 펴주고, 그 안에 user 객체 있으면 나머지를 바꿔주고, 내가 바꾸고 싶은 것만 정확하게 써준다. 안바꾸고 싶은 것은 오브젝트 스프레드로 참조관계 유지하기!

AppLayout 에 적용하기

useSelector는 react-redux이다.

const [isLoggedIn, setIsLoggedIn] = useState(false) 

→ 이제 이런거 필요 없다.

npm i react-redux

→ 리액트와 리덕스를 연결해준다. (for useSelector)

redux를 사용하면 useState를 사용하는 일이 좀 줄어든다.

profile
Step by step goes a long way ✨

0개의 댓글