메신저 웹(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의 사용을 권장하고 있다.

실제로 React-Redux 공식 문서에서도 Typescript를 활용할 경우 connect보다는 hooks의 사용을 권장하고 있다.

Redux 디렉토리 구조

디렉토리 구조


🦾 구현 내용 정리


Action

actionreducer를 실행시킨다!

기본 구조는 아래와 같다.

1
2
3
4
export const someAction = (data?) => ({
type : '',
payload: data?,
});

위에서 정의된 type은 하나의 action의 **고유 주소값(이름)**이라고 생각하면 된다.

그리고 payload는 이 action이 Dispatch에 의해 불려질 때 함께 담겨져서 따라온 data라고 볼 수 있다.

Reducer

Reducer는 직접적으로 state에 변화를 준다.

아래와 같이 switch문을 활용하여 action type에 맞는 로직이나 의도된 기능을 한 뒤, state에 변화를 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// redux/reducers/auth.ts

// 아래와 같이 초기 값을 지정해주는 것이 좋다.
const initialState = { users: [], currentUser: null };

export default function auth(state = initialState, action: any) {
switch (action.type) {
// action의 type이 GET_USERS일 때는 아래와 같이 기능
case GET_USERS:
return { ...state, users: action.payload };
// action의 type이 UPDATE~일 때는 아래와 같이 기능
case UPDATE_CURRENT_USER:
return { ...state, currentUser: action.payload };
// 어느 케이스에도 걸리지 않는다면 기존 state값을 반환한다.
default:
return state;
}
}

⚠️ 반드시 reducer는 export default 해줘야 오류없이 리듀서를 활용할 수 있다.

그리고 아래와 같이 리듀서가 여러개라면 아래와 같이 여러 리듀서를 하나로 묶어줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
// redux/reducers/index.ts

import { combineReducers } from 'redux';
import auth from './auth';

export type RootState = ReturnType<typeof rootReducer>;

const rootReducer = combineReducers({
auth,
});

export default rootReducer;

Store

store는 하나로 모아진 reducer를 담는 최종 상태관리 담당자라고 볼 수 있다.

코드는 생각보다 간단하다.

우선 rootReducer를 store에 담아준다.

1
2
3
4
5
6
7
8
// src/redux/store.ts

import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

export default store;

그리고 이후에 Provider를 통해 index.ts의 App컴포넌트를 감싸주면 초기 세팅은 끝이다.

1
2
3
4
5
6
7
8
9
10
11
12
// src/index.ts

// import라인 생략

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);

🌮 컴포넌트에서의 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> 문제로 고생했던 것은, statedispatch에 의해 업데이트가 되기도 전에 useSelectstate에 접근했기 때문이다. 따라서 data fetch → dispatch → useSelect가 될 수 있도록 위와 같은 코드 로직을 구현했다. ‼️

  1. 이제 컴포넌트에서 useSelect를 호출 해보자.

    1
    2
    const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Props {
user: User;
}
export const AuthProfileBox = ({ user }: Props) => {
// 1. dispatch를 아래와 같이 우선 dispatch라는 상수에 선언해주고
const dispatch = useDispatch();
// 3. 해당 Dispatch는 클릭된 유저의 유저데이터를 넘겨줘야 하기 때문에 아래와 같이 action에 user데이터를 넘겨준다.
const handleSetCurrentUser = () => {
dispatch(setCurrentUser(user));
};
return (
// 2. Profile박스를 클릭했을 때 dispatch가 실행되도록 한다.
<ProfileBox onClick={handleSetCurrentUser}>
<img src={user.profileImage} alt='profile' />
<h3>{user.userName}</h3>
</ProfileBox>
);
};

잘 이해가 되지 않는다면 아래의 action 코드 구조와 reducer 구조를 다시 확인해보자

  1. action

    1
    2
    3
    4
    5
    6
    7
    // 위에서 유저 프로필을 클릭했을 때 실행되는 액션이다.
    // 괄호 안에 있는 user가 중요하다. 이게 컴포넌트 측에서 담겨져서 와야하는 데이터를 의미한다.
    export const setCurrentUser = (user: User) => ({
    type: UPDATE_CURRENT_USER,
    // 그리고 그 넘겨져온 데이터를 payload라는 키에 할당해준다.
    payload: user,
    });
  2. reducer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export 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;
    }
    }

Author

Hoonjoo

Posted on

2022-02-13

Updated on

2022-02-14

Licensed under

Comments