메신저 웹(1/2)
🖐 프로젝트 소개
Redux, Typescript, CRA를 활용하여 메신저 웹앱을 개발했다.
사실 Typescript와 함께 Redux를 사용하는 것이 처음이었기에 많은 것들을 배울 수 있었다.
🧤 배포 주소
https://messenger-web-b98e6.web.app/
🎃 Redux의 활용
본 프로젝트에서는 Typescript + React를 사용했기 때문에
connect
가 아닌,hook
방식의 react-redux 상태관리 로직을 활용했다.
실제로 React-Redux 공식 문서에서도 Typescript를 활용할 경우 connect보다는 hooks의 사용을 권장하고 있다.
Redux 디렉토리 구조
🦾 구현 내용 정리
Action
action
은reducer
를 실행시킨다!
기본 구조는 아래와 같다.
1 | export const someAction = (data?) => ({ |
위에서 정의된 type
은 하나의 action의 **고유 주소값(이름)**이라고 생각하면 된다.
그리고 payload
는 이 action이 Dispatch에 의해 불려질 때 함께 담겨져서 따라온 data라고 볼 수 있다.
Reducer
Reducer는 직접적으로 state에 변화를 준다.
아래와 같이 switch문을 활용하여 action type에 맞는 로직이나 의도된 기능을 한 뒤, state에 변화를 준다.
1 | // redux/reducers/auth.ts |
⚠️ 반드시 reducer는 export default 해줘야 오류없이 리듀서를 활용할 수 있다.
그리고 아래와 같이 리듀서가 여러개라면 아래와 같이 여러 리듀서를 하나로 묶어줘야 한다.
1 | // redux/reducers/index.ts |
Store
store는 하나로 모아진 reducer를 담는 최종 상태관리 담당자라고 볼 수 있다.
코드는 생각보다 간단하다.
우선 rootReducer를 store에 담아준다.
1 | // src/redux/store.ts |
그리고 이후에 Provider를 통해 index.ts의 App컴포넌트를 감싸주면 초기 세팅은 끝이다.
1 | // src/index.ts |
🌮 컴포넌트에서의 state 활용(useSelect)
여기부터 실전이다.
useSelect
를 활용하여 컴포넌트 내에서store
에 있는state
에 접근할 수 있어야 한다.
하지만 이번 프로젝트에서는 초기 화면에서의 state
접근이 선행되는 것이 아니라, firestore에 있는 유저 데이터를 fetch
후 → dispatch
를 통해 store
에 유저데이터를 먼저 넣어줘야 했다.
그림으로 크게 보자면, 로직은 이와 같다.
App.tsx가 렌더링 되면 → useEffect
내에서 DB의 users데이터를 불러온다 → 그리고 그 데이터를 dispatch
에 담아서 → action
호출 → action
에 맞는 reducer
를 실행시킴 → 이제 users라는 state를 fetch
해온 users데이터로 업데이트 해주는 것이다.
‼️ 우리가 계속
Promise<pending>
문제로 고생했던 것은,state
가dispatch
에 의해 업데이트가 되기도 전에useSelect
로state
에 접근했기 때문이다. 따라서 data fetch → dispatch → useSelect가 될 수 있도록 위와 같은 코드 로직을 구현했다. ‼️
이제 컴포넌트에서 useSelect를 호출 해보자.
1
2const userData = useSelect((state) => state);
console.log(state);분명 오류가 날텐데, 이는 typescript에서 state에 타입을 지정해주지 않았기 때문에 발생하는 문제다.
따라서 위에서 언급했던 rootReducer 파일로 이동 후, RootState 타입을 생성 후
export
해줘야 한다.1
2
3
4// redux/reducers/index.ts
// 1. 이 코드를 rootReducer가 있는 파일에서 생성 후 export 해준다.
export type RootState = ReturnType<typeof rootReducer>;1
2
3
4
5// 2. 이제 state를 사용하길 원하는 컴포넌트로 이동한다.
// 3. 아래와 같이 state 매개변수에 RootState 타입을 지정해주면 더이상 타입오류가 나지 않는다.
const userData = useSelect((state: RootState) => state);
🧛♂️ 컴포넌트에서의 dispatch 활용(useDispatch)
Dispatch는 useDispatch를 통해 컴포넌트 내에서 action을 호출할 수 있다.
항상 action이 무슨 기능을 담당하는지 생각해야 한다. 유저를 업데이트하는 액션, 유저를 삭제하는 액션 등등 내가 필요한 액션을 명확히 정의한 후, dispatch를 통해 해당 액션을 호출하면 된다.
만약 이번 프로젝트에서 처럼, 내가 초기 화면에서 사용자 프로필을 눌렀을 때, 해당 사용자로 로그인 될 수 있도록 구현하고자 한다면?
프로필 박스의 onclick이벤트에 dispatch를 걸어주면 되는 것이다.
1 | interface Props { |
잘 이해가 되지 않는다면 아래의 action 코드 구조와 reducer 구조를 다시 확인해보자
action
1
2
3
4
5
6
7// 위에서 유저 프로필을 클릭했을 때 실행되는 액션이다.
// 괄호 안에 있는 user가 중요하다. 이게 컴포넌트 측에서 담겨져서 와야하는 데이터를 의미한다.
export const setCurrentUser = (user: User) => ({
type: UPDATE_CURRENT_USER,
// 그리고 그 넘겨져온 데이터를 payload라는 키에 할당해준다.
payload: user,
});reducer
1
2
3
4
5
6
7
8
9
10export default function auth(state = initialState, action: any) {
switch (action.type) {
// 위의 경우가 이 action type에 해당된다.
case UPDATE_CURRENT_USER:
// 따라서 해당 action의 payload(선택된 유저)를 currentUser라는 state 할당하며 업데이트 해주는 것이다.
return { ...state, currentUser: action.payload };
default:
return state;
}
}