티스토리 뷰

개발공부/🟪 Redux

[React] Redux를 활용한 상태 관리

2022. 9. 29. 00:59

 

 

Redux - 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너. | Redux

자바스크립트 앱을 위한 예측 가능한 상태 컨테이너.

ko.redux.js.org

 

 

 

 

1️⃣  Redux 소개

 

A Predictable State Container for JS Apps

 

JavaScript application들의 state(상태)를 관리하는 방법

 

-  앱 전체 상태를 쉽게 관리하기 위한 라이브러리이다.

-  Redux의 많은 개념들이 Flux pattern에서 차용되었다.

-  redux.js.org에서 수많은 문서를 참고할 수 있고, 웹상에 Redux를 활용한 앱 구축 사례가 많다.

-  React와 함께 사용되면서 유명해졌지만, React만을 위한 라이브러리가 아니다.  (React, Angular, Vue, Vanila JavaScript 등 JS언어 내에서 어디든 사용 가능)

 

 

 

 

▶ 언제 Redux를 써야하나

 

-  앱 전체의 상태 관리가 필요할 때

-  복잡한 비동기 처리가 있는 상태 관리가 필요할 때 (redux-thunk, redux-saga, redux-observable 등을 활용)

-  앱의 상태가 복잡하고, 이를 체계적으로 관리하고 싶을 때

-  상태 관리 패턴을 도입하여, 여러 개발자와 협업하고 싶을 때

-  logger, devtool 등을 활용하여 상태를 관리할 필요가 있을 때

 

 

 

 

 

▶  Redux의 핵심 원칙

 

Single source of truth 

:  Store는 단 하나이며, 모든 앱의 상태는 이곳에 보관된다.

 

Immutability 

:  상태는오로지 읽을 수만 있다. 변경하려면 모든 상태가 변경되어야 한다. (변경을 만들려면 dispatch를 통해 변경해야 한다)

 

Pure function 

:  상태의 변경은 어떠한 사이드 이펙트도 만들지 않아야 한다.

 

 

 

 

 

▶  Redux 기본 개념

 

Action

:  상태의 변경을 나타내는 개념

 

-  어떤 형태든지 상관없으나 주로 type, payload를 포함하는 JavaScript 객체이다.

const action1 = {
    type: 'namespace/getMyData',
    payload : {
        id: 123
    }
}

 

 

 

Action Creator

:  Action을 생성하는 함수

 

-  직접 Action을 생성하는 것보다 Action Creator를 활용하면 재사용성이 좋고, 하나의 레이어를 추가할 수 있다.

const addObj = (id) => ({
    type: 'namespace/getMyData',
    payload : {
        id: String(id).slice(1)
    }
})

 

 

 

Store

:  앱 전체의 상태를 보관하는 곳

 

-  Action에 따라 reducer에서는 새로운 상태를 만들어내며, Store는 그 상태를 저장한다.

-  Store의 상태는 불변하며 매 액션이 발생할 때마다 새로운 객체가 만들어진다. (reducer에서 stroe로 넘어가는 상태는 항상 새로운 객체)

const store = createStore(reducer, initialState)

 

 

 

Reducer

:  Action을 받아 새로운 State를 생성

 

-  (state, action) => state의 인터페이스를 따른다.

-  상태 변경 시 사이드 이펙트가 없어야 한다.

-  reducer에서는 상태를 변경하면 안되고, 새로운 상태를 리턴해야만 한다.

const reducer = (state, action) => {
    switch (action.type) {
        case 'namespace/getMyData':
            const obj = { id: action.payload.id }
            return { ...state, obj }
        default:
            return state
    }
}

const store = createStore(reducer, initialState)

 

 

 

Dispatch

:  Action을 reducer로 보내는 함수

 

-  dispatch 후에 action은 middleware를 거쳐 reducer에 도달한다.

function MyApp() {
    const dispatch = useDispatch()
    return (
        <button
            onClick={() => dispatch(addObj(1234))}
        >Submit</button>
    )
}

 

 

 

 

Selector

:  특정 state 조각을 store로부터 가져오는 함수

 

-  store의 state는 raw data를 저장하고, 계산된 값 등을 selector로 가져오는 등의 패턴을 구사할 때 유용하다.

function MyApp() {
    const obj = useSelector(state=> state.obj)
    return (
        <div>
            {JSON.stringify(obj)}
        </div>
    )
}

 

 

 

 

 

 

 

 

2️⃣  Redux의 구조

 

-  redux는 자유롭게 확장하여 사용할 수 있다.

-  내부적으로 action과 데이터가 어떻게 흐르는지 이해하고 middleware, enhancer 등을 이용하여 redux를 확장하여 사용할 수 있다.

 

 

💡  redux에서의 middleware

-  action은 dispatch 이후 모든 middleware를 먼저 통과한 후에 reducer에 도달한다.

-  redux-thunk, redux-logger 등의 라이브러리를 적용 가능하다.

-  middleware에서는 사이드이펙트가 있어도 된다.

 

 

 

💡  redux에서의 enhancer

-  action은 dispatch 이후 모든 middleware를 먼저 통과한 후에 reducer에 도달한다.

-  enhancer는 redux devtools과 같이 전체 state를 대상으로하여 redux의 동작을 확장한다.

 

 

 

 

 

▶  Redux의 구조

 

이미지출처 : 엘리스 React 심화 Ⅱ - 01 Redux를 활용한 상태 관리 강의 자료

 

 

 

 

 

 

 

 

3️⃣  redux-toolkit

 

-  redux에서 공식적으로 추천하는, helper 라이브러리이다.

-  기존에 만들어야 하는 수많은 보일러 플레이트를 제거하고, 유용한 라이브러리를 포함하여 redux 코드를 쉽게 작성하게 한다.

-  redux-devtools, immerjs, redux-thunk, reselect 등의 라이브러리가 미리 포함되어 있다.

 

 

 

 

▶  redux-toolkit  API

 

configureStore()

:  redux의 createStore 함수를 래핑

 

-  named parameter로 쉽게 store를 생성한다.

-  reducer 객체를 받아 combineReducers를 적용한다. (서로 다른 reducer들을 객체로 받아 하나의 리듀싱 함수로 바꿔줌)

const store = configureStore({
    reducer: {
        posts: postsReducer,
        users: usersReducer
    }
})

 

 

 

 

createAction()

:  action creator를 만드는 함수

 

-  만들어진 action creator에 데이터를 넘기면 payload 필드로 들어간다.

-  생성된 action creator는 toString() 메서드를 오버라이드하여 자신이 생성하는 액션의 타입 String을 리턴한다.

const addPost = createAction('post/addPost')

addPost({ title: 'post 1' })
/*
{
    type: 'post/addPost',
    payload : { title : 'post 1' }
}
*/

 

 

 

 

createReducer()

:  reducer를 만드는 함수

 

-  builder의 addCase 메서드를 이용하여 action마다 state의 변경을 정의한다.

-  immerjs를 내부적으로 사용하므로 mutable code를 이용해 간편하게 변경 코드를 작성할 수 있다.

const postsReducer = createReducer(initState, 
    builder => {
        builder.addCase(addPost, (state, action) => {
            state.posts.push(action.payload)
        })
    })

 

 

 

 

createSlice()

:  Slice는 Action creator, reducer 등 별도로 만들어야 하는 여러 Redux 구현체를 하나의 객체로 모은 것

 

-  createSlice 함수를 이용하여 많은 보일러 플레이트를 없애고 쉽게 action creator, reducer를 만들 수 있다.

-  reducers를 정의하면 각 Reducer 함수의 이름 자체가 action creator가 된다.

const postsSlice = createSlice({
    name : 'posts',
    initialState,
    reducers: {
        addPost(state, action) {
            state.posts.push(action.payload)
        }
    }
})

const { addPost } = postsSlice.actions
const reducer = postsSlice.reducer

 

 

 

 

createSelector()

:  createSelector 함수를 이용해 state를 이용한 특정 데이터를 리턴

 

-  내부적으로 데이터를 캐시하며 데이터가 변동이 없다면 캐시된 데이터를 리턴한다. (redux에서 알아서 캐시를 관리하여 성능 향상)

const postsSelector = state => state.posts

const userSelector = state => state.user

const postsByUserIdSelector = createSelector(
    postsSelector, 
    userSelector, 
    (posts, user) => 
        posts.filter(post => 
            post.username === user.username
        )
)

 

 

 

 

 

 

 

4️⃣  Redux를 React에 연결하기

 

 

▶  react-redux

 

redux를 react 앱에 연결하게 하는 라이브러리

 

-  redux에서 관리하는 상태, dispatch 함수 등을 가져올 수 있다.

-  클래스 컴포넌트, 함수형 컴포넌트에 모두 연결할 수 있다.

 

 

 

 

▶  react-redux  API

 

Provider

 

-  Redux store를 React와 연결하기 위해서는 반드시 Provider로 컴포넌트를 감싸야만 한다.

-  Provider 안에서 렌더링된 컴포넌트들은 state에 접근할 수 있다.

const store = configureStore({
    reducer: rootReducer
})

function App() {
    return (
        <Provider store={store}>
            <MyPage />
        </Provider>
    )
}

 

 

 

 

useDispatch

:  redux의 dispatch 함수를 가져오기 위한 API

 

-  dispatch로 action creator가 생성한 action을 보내면 redux 내부로 보내지게 된다.

const addPost = createAction('addPost')
    function MyPage() {
        const dispatch = useDispatch()
        
        const handleClick = () =>
            dispatch(addPost())
            
        return (
            <button 
                onClick={handleClick}
            >Submit</button>
    )
}

 

 

 

 

useSelector

:  Redux store로부터 데이터를 얻기 위한 API

 

-  selector function을 인자로 넘긴다.

-  selector function은 데이터에 어떤 변경을 가하면 안 된다.

-  데이터를 특정 형태로 계산하여 읽을 수 있다.

function MyPage() {
    const posts = useSelector(state => state.posts)
    
    return posts.map(post => <Post {...post} />)
}

 

 

 

 

 

 

 

5️⃣  Redux를 이용한 비동기 처리

 

-  redux는 비동기 처리를 자체 내장하지 않고, 비동기 처리를 위해서는 비동기를 위한 middleware를 추가하여야 한다.

-  redux-thunk는 Promise를 이용한 비동기 Action을 쉽게 처리하도록 하는 middleware이다.

-  redux-saga, redux-observable 등의 라이브러리를 활용할 수도 있다.

 

 

 

본 강의에서는 redux-toolkit을 활용하여 비동기 처리를 진행할 예정이다.

redux-toolkit에는 thunk middleware를 디폴트로 추가되어 있다.

 

 

 

 

▶  createAsyncThunk()

 

:  fulfilled, rejected, pending 3가지 상태에 대해 각각 reducer를 작성

 

-  redux-toolkit에서 제공하는 API이다.

-  TypeScript 환경에서 reducer 작성 시, builder callback을 사용하여 작성해야 정확한 타이핑이 가능하다.

 

 

const addPost = createAsyncThunk('posts/addPost',
    async (title) => {
        const result = await PostAPI.addPost({ title })
        
        return result.data
    }
)


// Component
useEffect(() => {
    dispatch(addPost("post 1"))
}, [])

-  createAsyncThunk는 두 인자 action type, async callback(payload creator)를 받는다.

-  action type이 주어지면 pending, fulfilled, rejected가 각각 postfix로 붙어 reducer로 들어온다.   ex) posts/addPost/pending

 

 

 

const addPost = createAsyncThunk('posts/addPost',
    async (title) => {
        const result = await PostAPI.addPost({ title })
        
        return result.data
    }
)

-  createAsyncThunk로 만들어진 action creator는 4가지 함수로 구성된다.

  • addPost  :  async 함수를 dispatch하는 함수 (createAsyncThunk()가 반환하는 함수)
  • addPost.pending  :  promise를 생성했을 때 발생하는 액션
  • addPost.fulfilled  :  promise가 fulfilled 되었을 때 발생하는 액션
  • addPost.rejected  :  promise가 rejected 되었을 때 발생하는 액션

 

 

const postsSlice = createSlice({
    // ...
    extraReducers: builder => {
        builder
            .addCase(addPost.pending, state => 
        ...)
            .addCase(addPost.fulfilled, state => 
        ...)
            .addCase(addPost.rejected, state => 
        ...)
    }
})

-  createSlice의 extraReducers 함수를 이용해 builder에 각 상황에 대한 reducer를 추가한다.

-  공식적으로 builder pattern을 추천하는데 타입스크립트에서 타이핑이 용이하기 때문이다.

-  fulfilled 시 데이터는 payload로 들어온다.    ex) action.payload.todos

-  rejected 시 에러는 action.error로 들어오며 payload는 undefined가 된다.

 

 

 

 

 

▶  연속적인 비동기 처리

 

thunk 함수를 dispatch하면 promise가 리턴된다.

따라서 .then() 메서드로 연속적인 비동기 처리를 이어 실행 가능하다.

dispatch(addPost("post1"))
    .then(() => 
    
dispatch(updatePost("post2")))

 

 

 

 

▶  동시 비동기 처리

 

Promise.all 을 이용해 여러 비동기 처리를 동시에 실행한다.

주의할 점은 thunk의 promise가 rejected 되어도 .then() 으로 들어온다는 것이다.

Promise.all([
    dispatch(addPost("post1")),
    dispatch(updatePost("post2"))
])
    .then(() => 
console.log("DONE"))

 

 

 

 

 

 

 


 이 글은 엘리스의 AI트랙 5기 강의를 들으며 정리한 내용입니다.

반응형
프로필사진
개발자 삐롱히

프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그