코드 구조
이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
Redux FAQ: 코드 구조
파일 구조는 어떻게 구성해야 하나요? 액션 생성자와 리듀서를 프로젝트에서 어떻게 그룹화해야 하며, 셀렉터는 어디에 배치해야 하나요?
Redux는 단순한 데이터 저장 라이브러리이므로 프로젝트 구조에 대해 특정 의견을 강요하지 않습니다. 다만 대부분의 Redux 개발자들이 주로 사용하는 몇 가지 일반적인 패턴이 있습니다:
-
Rails 스타일: "actions", "constants", "reducers", "containers", "components"를 위한 별도의 폴더 구성
-
"기능 폴더" / "도메인" 스타일: 기능이나 도메인별로 별도의 폴더를 구성하고, 필요시 파일 유형별 하위 폴더를 생성
-
"Ducks/Slices": 도메인 스타일과 유사하지만 액션과 리듀서를 명시적으로 결합(주로 동일 파일 내 정의)
일반적으로 셀렉터는 리듀서와 함께 정의하여 내보낸 후, 다른 곳(mapStateToProps 함수, 비동기 액션 생성자, 사가 등)에서 재사용하는 것이 권장됩니다. 이렇게 하면 상태 트리의 실제 구조를 아는 모든 코드가 리듀서 파일에 집중됩니다.
특히 로직을 "기능 폴더"로 구성하고, 주어진 기능에 대한 모든 Redux 로직을 단일 "slice/ducks 파일"에 배치하는 것을 권장합니다.
예시는 다음 섹션을 참조하세요:
Detailed Explanation: Example Folder Structure
An example folder structure might look something like:
/srcindex.tsx: Entry point file that renders the React component tree/appstore.ts: store setuprootReducer.ts: root reducer (optional)App.tsx: root React component
/common: hooks, generic components, utils, etc/features: contains all "feature folders"/todos: a single feature foldertodosSlice.ts: Redux reducer logic and associated actionsTodos.tsx: a React component
/app contains app-wide setup and layout that depends on all the other folders.
/common contains truly generic and reusable utilities and components.
/features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators.
코드를 디스크에 어떻게 배치하든 최종적으로 중요하지 않지만, 액션과 리듀서를 분리해서 고려해서는 안 된다는 점을 기억해야 합니다. 한 폴더에 정의된 리듀서가 다른 폴더에서 정의된 액션에 반응하는 것은 전적으로 가능하며(권장됩니다).
추가 정보
문서
아티클
-
React 애플리케이션 확장 방법 (관련 발표: 애플리케이션 확장)
토론
리듀서와 액션 생성자 간 로직을 어떻게 나눠야 하나요? "비즈니스 로직"은 어디에 배치해야 하나요?
로직의 어떤 부분을 리듀서에 넣을지 액션 생성자에 넣을지에 대해 명확한 단일 답은 없습니다. 어떤 개발자들은 "비대한(fat)" 액션 생성자와 단순히 액션의 데이터를 받아 해당 상태에 무조건 병합하는 "얇은(thin)" 리듀서를 선호합니다. 다른 이들은 액션을 최대한 작게 유지하고 액션 생성자에서 getState() 사용을 최소화하려고 합니다. (이 질문의 목적상 사가(sagas) 및 옵저버블(observables)과 같은 다른 비동기 접근 방식은 "액션 생성자" 범주에 속합니다.)
로직을 리듀서에 더 많이 넣으면 여러 잠재적 이점이 있습니다. 액션 타입이 더 의미론적이고 의미 있게 될 가능성이 높습니다(예: "SET_STATE" 대신 "USER_UPDATED"). 또한 리듀서에 더 많은 로직이 있다는 것은 시간 여행 디버깅으로 인해 더 많은 기능이 영향을 받을 수 있음을 의미합니다.
이 댓글이 이 이분법을 잘 요약합니다:
문제는 액션 생성자에 무엇을 넣고 리듀서에 무엇을 넣을지, 즉 비대한 액션 객체와 얇은 액션 객체 사이의 선택입니다. 모든 로직을 액션 생성자에 넣으면 기본적으로 상태 업데이트를 선언하는 비대한 액션 객체가 생성됩니다. 리듀서는 순수하고, 단순하며, 추가/제거/업데이트 함수가 됩니다. 구성하기는 쉬우나 비즈니스 로직 대부분이 여기에 없을 것입니다. 반면 리듀서에 더 많은 로직을 넣으면 깔끔하고 얇은 액션 객체가 생성되고 대부분의 데이터 로직이 한 곳에 모이지만, 다른 분기(branch)의 정보가 필요할 수 있어 리듀서 구성이 어려워집니다. 결국 거대한 리듀서나 상태 상위에서 추가 인수를 받는 리듀서가 생깁니다.
가능한 한 많은 로직을 리듀서에 배치하는 것을 권장합니다. 액션에 포함될 내용을 준비하는 데 도움이 되는 로직이 필요한 경우도 있지만, 대부분의 작업은 리듀서가 수행해야 합니다.
추가 정보
문서
아티클
토론
액션 생성자를 왜 사용해야 하나요?
Redux는 액션 생성자를 반드시 사용해야 한다고 강제하지 않습니다. 객체 리터럴을 dispatch에 직접 전달하는 방식을 포함해, 여러분에게 가장 편리한 방식으로 액션을 생성할 수 있습니다. 액션 생성자는 Flux 아키텍처에서 비롯되었으며, 다음과 같은 여러 이점으로 인해 Redux 커뮤니티에서 채택되었습니다.
액션 생성자는 유지보수가 더 쉽습니다. 액션에 대한 업데이트는 한 곳에서 수행되고 모든 곳에 적용됩니다. 모든 액션 인스턴스는 동일한 형태와 기본값을 보장받습니다.
액션 생성자는 테스트하기 용이합니다. 인라인 액션의 정확성은 수동으로 검증해야 합니다. 반면 모든 함수와 마찬가지로 액션 생성자에 대한 테스트는 한 번 작성하면 자동으로 실행할 수 있습니다.
액션 생성자는 문서화하기 더 쉽습니다. 액션 생성자의 매개변수는 액션의 의존성을 명시적으로 나열합니다. 또한 액션 정의를 중앙에서 관리하면 문서화 주석을 작성하기에 편리한 장소가 제공됩니다. 액션이 인라인으로 작성된 경우 이 정보를 포착하고 전달하기가 더 어렵습니다.
액션 생성자는 더 강력한 추상화 수단입니다. 액션 생성은 종종 데이터 변환이나 AJAX 요청을 수반합니다. 액션 생성자는 이러한 다양한 로직에 대해 일관된 인터페이스를 제공합니다. 이 추상화는 컴포넌트가 액션 생성의 세부 사항에 얽매이지 않고 액션을 디스패치할 수 있도록 해줍니다.
추가 정보
아티클
토론
웹소켓과 같은 지속적 연결은 어디에 배치해야 할까요?
미들웨어는 Redux 앱에서 웹소켓 같은 영속적 연결을 처리하기에 가장 적합한 위치입니다. 그 이유는 다음과 같습니다:
-
미들웨어는 애플리케이션 수명 주기 동안 존재합니다
-
스토어 자체와 마찬가지로 전체 앱에서 사용할 단일 연결 인스턴스만 필요합니다
-
미들웨어는 디스패치된 모든 액션을 확인하고 직접 액션을 디스패치할 수 있습니다. 즉 디스패치된 액션을 가져와 웹소켓으로 전송되는 메시지로 변환하고, 웹소켓에서 메시지를 수신하면 새 액션을 디스패치할 수 있습니다.
-
웹소켓 연결 인스턴스는 직렬화할 수 없으므로 스토어 상태 자체에 포함시키면 안 됩니다
소켓 미들웨어가 Redux 액션을 디스패치하고 응답하는 방식을 보여주는 예시를 참고하세요.
웹소켓 및 유사한 연결을 위한 기존 미들웨어가 많이 있습니다 - 아래 링크를 확인하세요.
라이브러리
비-컴포넌트 파일에서 Redux 스토어를 어떻게 사용하나요?
애플리케이션당 단일 Redux 스토어만 존재해야 합니다. 이는 사실상 애플리케이션 아키텍처에서 싱글톤으로 동작합니다. React와 함께 사용할 때는 루트 <App> 컴포넌트 주위에 <Provider store={store}>를 렌더링하여 런타임에 컴포넌트에 스토어를 주입하므로, 애플리케이션 설정 로직만 스토어를 직접 임포트하면 됩니다.
하지만 코드베이스의 다른 부분에서도 스토어와 상호작용해야 할 때가 있습니다.
다른 코드베이스 파일에서 스토어를 직접 임포트하는 것은 피해야 합니다. 경우에 따라 동작할 수 있지만, 종종 순환 임포트 의존성 오류를 발생시킵니다.
가능한 해결 방법은 다음과 같습니다:
-
스토어 의존 로직을 썽크(thunk)로 작성한 다음 컴포넌트에서 해당 썽크를 디스패치
-
컴포넌트에서
dispatch참조를 인수로 관련 함수에 전달 -
로직을 미들웨어로 작성하고 설정 시점에 스토어에 추가
-
애플리케이션 생성 시점에 스토어 인스턴스를 관련 파일에 주입
일반적인 사용 사례로 Axios 인터셉터 내부에서 Redux 상태의 토큰 같은 API 인증 정보를 읽는 경우가 있습니다. 인터셉터 파일은 store.getState()를 참조해야 하지만, API 계층 파일에서도 임포트되므로 순환 임포트가 발생합니다.
대신 인터셉터 파일에서 injectStore 함수를 노출할 수 있습니다:
let store
export const injectStore = _store => {
store = _store
}
axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})
그런 다음 진입점 파일에서 API 설정 파일에 스토어를 주입합니다:
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)
이렇게 하면 애플리케이션 설정 코드만 스토어를 임포트하면 되며, 파일 의존성 그래프에서 순환 의존성을 피할 수 있습니다.