초보 개발자의 성장 일기

Redux 본문

Development/React JS

Redux

YUNA 2023. 11. 29. 10:22

Redux

앱 전체 상태를 쉽게 관리하기 위한 라이브러리이다. Redux의 많은 개념들이 Flux pattern에서 차용된다. 주로 React 앱과 같이 사용한다.

  • 앱 전체의 상태 관리가 필요할 때 사용한다.
  • 복잡한 비동기 처리가 있는 상태 관리가 필요할 때 사용한다.
  • 앱의 상태가 복잡하고, 이를 체계적으로 관리하고 싶을 때 사용한다.
  • 상태 관리 패턴을 도입하여, 여러 개발자와 협업하고 싶을 때 사용한다.
  • logger, devtool 등을 활용하여 상태를 관리할 필요가 있을 때 사용한다.

핵심 원칙

  • Single source of truth - Store는 단 하나이며, 모든 앱의 상태는 이곳에 보관된다.
  • Immutability – 상태는 오로지 읽을수만 있다. 변경하려면 모든 상태가 변경되어야 한다.
  • Pure function – 상태의 변경은 어떠한 사이드 이펙트도 만들지 않아야 한다.

사이드 이펙트는 직역하면 부작용이라는 의미로, 예상하지 못하는 상황에서 생기는 문제

 

Action

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

Action은 상태의 변경을 나타낸다. 어떤 형태든지 상관없으나, 주로 type, payload를 포함하는 JavaScript 객체이다.

 

Action Creator

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

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

 

Store

const store = createStore(reducer, initialState)

앱 전체의 상태를 보관하는 곳이다. Action에 따라 reducer에서는 새로운 상태를 만들어내며, Store는 그 상태를 저장한다. Store의 상태는 불변하며, 매 액션이 발생할 때마다 새로운 객체가 만들어진다.

 

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)

Action을 받아 새로운 State를 만든다. (state, action) => state의 인터페이스를 따르고 상태 변경 시 사이드 이펙트가 없어야 한다.

 

Dispatch

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

Action을 redux로 보내는 함수이다. dispatch 후에 action은 middleware를 거쳐 reducer에 도달한다.

 

Selector

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

특정 state 조각을 store로부터 가져오는 함수이다. store의 state는 raw data를 저장하고, 계산된 값 등을 selector로 가져오는 등의 패턴을 구사할 때 유용하다.

 

 

구조

middleware

action은 dispatch 이후 모든 middleware를 먼저 통과한 후에 reducer에 도달한다. redux-thunk, redux-logger 등의 라이브러리를 적용한다.

 

enhancer

action은 dispatch 이후 모든 middleware를 먼저 통과한 후에 reducer에 도달한다. redux devtools 등의 라이브러리를 적용한다.

 

 

redux-toolkit

redux에서 공식적으로 추천하는, helper 라이브러리이다. 기존에 만들어야 하는 수많은 보일러 플레이트를 제거하고, 유용한 라이브러리를 포함하여 redux 코드를 쉽게 작성하게 한다. redux-devtools, immerjs, redux-thunk, reselect 등의 라이브러리가 미리 포함된다.

 

redux-toolkit API - configureStore

const store = configureStore({
    reducer: {
        posts: postsReducer,
        users: usersReducer
    }
})
  • redux의 createStore 함수를 래핑한다.
  • named parameter로 쉽게 store를 생성한다.
  • reducer - 객체를 받아, combineReducers를 적용한다.

 

redux-toolkit API - createAction

const addPost =
createAction('post/addPost')
addPost({ title: 'post 1' })
/*
{
    type: 'post/addPost',
    payload : { title : 'post 1' }
}
*/
  • Action creator를 만드는 함수이다.
  • 만들어진 action creator에 데이터를 넘기면, payload 필드로 들어간다.
  • 생성된 action creator는 toString() 메서드를 오버라이드해, 자신이 생성하는 액션의 타입 String을 리턴한다.

 

redux-toolkit API - createReducer

const postsReducer =
    createReducer(initState,
        builder => {
        	builder.addCase(addPost,
        		(state, action) => {
        			state.posts
        				.push(action.payload)
        })
})
  • reducer를 만든다.
  • builder의 addCase 메서드를 이용하여, action마다 state의 변경을 정의한다.
  • immerjs를 내부적으로 사용하므로, mutable code를 이용해 간편하게 변경 코드를 작성한다.

 

redux-toolkit API - createSlice

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

const { addPost } = postsSlice.actions
const reducer = postsSlice.reducer
  • Slice는 Action creator, reducer 등 별도로 만들어야 하는 여러 Redux 구현체를 하나의 객체로 모은 것이다.
  • createSlice 함수를 이용하여, 많은 보일러 플레이트를 없애고 쉽게 action creator, reducer를 만든다.

 

redux-toolkit API - createSelector

const postsSelector = state => state.posts

const userSelector = state => state.user

const postsByUserIdSelector = createSelector(
    postsSelector,
    userSelector,
    (posts, user) =>
    	posts.filter(post =>
    		post.username === user.username
    )
)
  • createSelector 함수를 이용해, state를 이용한 특정 데이터를 리턴하도록 한다.
  • 내부적으로 데이터를 캐시하며, 데이터가 변동이 없다면 캐시된 데이터를 리턴한다.

 

react-redux

redux를 react 앱에 연결하게 하는 라이브러리이다. redux에서 관리하는 상태로 dispatch 함수 등을 가져올 수 있고 클래스 컴포넌트, 함수형 컴포넌트에 모두 연결할 수 있다.

 

react-redux API - Provider

const store = configureStore({
	reducer: rootReducer
})

function App() {
    return (
        <Provider store={store}>
        	<MyPage />
        </Provider>
    )
}
  • Redux store를 React와 연결하기 위해서는 반드시 Provider로 컴포넌트를 감싸야만 한다.
  • Provider 안에서 렌더링된 컴포넌트들은 state에 접근할 수 있다.

 

react-redux API - useDispatch

const addPost = createAction('addPost')

function MyPage() {
    const dispatch = useDispatch()
    
    const handleClick = () =>
        dispatch(addPost())
    
    return (
        <button
        	onClick={handleClick}
        >Submit</button>
    )
}
  • redux의 dispatch 함수를 가져오기 위한 API이다.
  • dispatch로 action creator가 생성한 action을 보내면 redux 내부로 보내지게 된다.

 

react-redux API - useSelector

function MyPage() {
	const posts = useSelector(state
=> state.posts)

    return posts.map(
        post => <Post {...post} />)
}
  • Redux store로부터 데이터를 얻기 위한 API이다.
  • selector function을 인자로 넘긴다.
  • selector functio은 데이터에 어떤 변경을 가하면 안 된다.
  • 데이터를 특정 형태로 계산하여 읽을 수 있다.

 

Redux를 이용한 비동기 처리

redux 비동기 처리를 위해서는 비동기를 위한 middleware를 추가하여야 한다. redux-thunk는 Promise를 이용한 비동기 Action을 쉽게 처리하도록 하는 middleware이다.

 

createAsyncThunk

  • redux-toolkit에서는 thunk middleware를 디폴트로 추가한다.
  • redux-toolkitcreateAsyncThunk API를 제공한다.
  • fulfilled, rejected, pending 3가지 상태에 대해 각각 reducer를 작성한다.
  • 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하는 함수이다.
  • 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에 각 상황에 대한 리듀서를 추가한다.
  • 공식적으로 builder pattern을 추천하는데, 타입스크립트에서 타이핑이 용이하기 때문이다.
  • fulfilled시 데이터는 payload로 들어온다. ex) action.payload.todos
  • rejected시 에러는 action.error로 들어오며, payloadundefined가 된다.

연속적인 비동기 처리

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

dispatch(updatePost("post2")))
  • thunk 함수를 dispatch하면 promise가 리턴한다.
  • .then() 메서드로 연속적인 비동기 처리를 이어 실행한다.

동시 비동기 처리

Promise.all([
    dispatch(addPost("post1")),
    dispatch(updatePost("post2"))
])
	.then(() =>
console.log("DONE"))
  • Promise.all 을 이용해, 여러 비동기 처리를 동시에 실행한다.
  • 주의할 점은, thunk의 promise가 rejected 되어도 .then() 으로 들어온다는 것이다.

 

#엘리스트랙 #엘리스트랙후기 #리액트네이티브강좌 #온라인코딩부트캠프 #온라인코딩학원 #프론트엔드학원 #개발자국비지원 #개발자부트캠프 #국비지원부트캠프 #프론트엔드국비지원 #React #Styledcomponent #React Router Dom #Redux #Typescript #Javascript

'Development > React JS' 카테고리의 다른 글

useRef  (0) 2023.11.30
useEffect  (0) 2023.11.30
리액트 상태관리  (0) 2023.11.27
비동기 통신과 Promise  (0) 2023.11.24
리액트와 라우터의 관계  (0) 2023.11.22