앞에서 Todo 앱을 만들때처럼 Modal 을 만들기 위해 redux 를 사용해보자. redux 를 내 것으로 만들기 위해 이런저런 코드들을 직접 redux 로 구현해보고자 한다.
모달에 필요한 것은 전체 대화 item 리스트와 모달을 나타낼지 여부이다.
각 대화 item 은 다음의 정보를 저장해야 한다.
또한 모달을 나타낼지 여부를 저장해야 한다.
이처럼 채팅에 대한 상태와 app state
모달을 띄우는지 여부를 나타내는 상태 UI state
로 나뉜다.
각 채팅 아이템은 다음의 필드가 필요할 것이다.
모달이 열렸는지 여부는 다음 열거값들로 설명할 수 있다.
Opened
, Closed
const initialState = {
chats: [
{ id: 168568774, role: "SYSTEM", text: '무엇을 도와드릴까요?'},
],
modal: {
status: 'Opened'
}
}
위의 액션 리스트를 type 과 payload 형식으로 전환한 형식을 생각해봤다.
{ type: 'modals/modalOpened' }
{ type: 'modals/modalClosed' }
{ type: 'chats/userChatPosted', payload: text }
{ type: 'chats/systemChatPosted', payload: text }
sudo npm i @reduxjs/toolkit
sudo npm i redux
sudo npm i react-redux
toolkit 과 redux 를 설치해줬다.
그리고 toolkit 의 createSlice 를 사용하고자 한다.
toolkit 의 createSlice 를 통해 constant, action, reducer, selector 들을 관리하는 하나의 파일을 만들어 보자.
먼저 concept / area 별로 features 를 나누기 위해 modals 와 chats 로 나누어 reducer slice file 을 만들어 보자.
// features/chats/chatsSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
chats: [{ id: "12345667", role: "SYSTEM", text: "무엇을 도와드릴까요?" }],
};
const chatsSlice = createSlice({
name: "chats",
initialState,
reducers: {
systemChatPosted(state, action) {
state.chats.push({
id: new Date().toDateString,
role: "SYSTEM",
text: action.payload,
});
},
userChatPosted(state, action) {
state.chats.push({
id: new Date().toDateString,
role: "USER",
text: action.payload,
});
},
},
});
export const chats = (state) => state.chats;
export const { systemChatPosted, userChatPosted } = chatsSlice.actions;
export default chatsSlice.reducer;
// featrues/modals/modalsSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
isOpened: false,
};
const modalsSlice = createSlice({
name: "modals",
initialState,
reducers: {
modalOpened(state) {
state.isOpened = true;
},
modalClosed(state) {
state.isOpened = false;
},
},
});
export const modalStatus = (state) => state.modals.isOpened;
export const { modalOpened, modalClosed } = modalsSlice.actions;
export default modalsSlice.reducer;
// features/modals/Modal.jsx
import { useDispatch, useSelector } from "react-redux";
import { modalClosed } from "./modalsSlice";
import { modalStatus } from "./modalsSlice";
import { systemChatPosted, userChatPosted, chats } from "../chats/chatsSlice";
import { useState } from "react";
export function Modal() {
const dispatch = useDispatch();
const chatList = useSelector(chats);
const [inputText, setInputText] = useState("");
const closeButttonStyle = {
background: "black",
color: "white",
border: "none",
borderRadius: "20px",
margin: "5px",
padding: "10px",
cursor: "pointer",
};
const chatBox = {
padding: "10px 12px",
display: "inline-flex",
borderRadius: "12px 12px 12px 0px",
border: "1px solid #BDBDBD",
width: "fit-content",
marginBottom: "10px",
};
const chatStyle = (role) =>
role === "SYSTEM"
? { ...chatBox, backgroundColor: "blue", color: "white" }
: {
...chatBox,
backgroundColor: "#FEF01B",
color: "black",
borderRadius: "12px 12px 0px 12px",
alignSelf: "flex-end",
};
const containerStyle = {
backgroundColor: "white",
width: "400px",
height: "500px",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
display: "grid",
gridTemplateRows: "1fr 15fr 2fr",
zIndex: 100,
borderRadius: "16px",
border: "1px solid #BDBDBD",
padding: "25px",
};
const modalHeaderStyle = {
display: "grid",
gridTemplateColumns: "8fr 1fr",
};
const inputTextStyle = {
color: "#6B6B6B",
fontFamily: "Roboto",
fontSize: "16px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "24px",
padding: "10px 12px",
borderRadius: "10px",
};
const askButtonStyle = {
display: "flex",
padding: "10px 20px",
justifyContent: "center",
alignItems: "center",
borderRadius: "12px",
border: "1px solid #BDBDBD",
background: "#FC9162",
color: "white",
fontFamily: "Roboto",
fontSize: "14px",
fontWeight: 500,
lineHeight: "20px",
cursor: "pointer",
};
return (
<div style={containerStyle}>
<div style={modalHeaderStyle}>
<div></div>
<button
style={closeButttonStyle}
onClick={() => dispatch(modalClosed())}
>
X
</button>
</div>
<ul style={{ display: "flex", flexDirection: "column" }}>
{chatList.chats.map((chatItem) => (
<li key={chatItem.id} style={chatStyle(chatItem.role)}>
{chatItem.text}
</li>
))}
</ul>
<textarea
type="text"
placeholder="지난주 울산신항에 입항했던 선박들에 대한 정보를 알려줘"
onChange={(e) => {
if (e.target.value.length <= 100) setInputText(e.target.value);
}}
value={inputText}
style={inputTextStyle}
/>
<div
style={{ lineHeight: 2.5, display: "flex", justifyContent: "flex-end" }}
>
{inputText.length} / 100
</div>
<button
style={askButtonStyle}
onClick={() => {
dispatch(userChatPosted(inputText));
setInputText("");
}}
>
요청하기
</button>
</div>
);
}
css 에 대한 고민이 항상 든다. 아직 Modal 컴포넌트를 UI 만 보려고 styled-component 든, CSS Module 과 같은 방식을 적용하지는 않았는데 기왕이면 scss 를 React 에서 사용하고 싶다.
한번 redux 로 실제 돌아가는 코드를 짜본 뒤라 더욱 redux 를 쓰기 용이했던 것 같다.
들어간 reducer 가 modal on / off, chat post 기능뿐인 간단한 모달이었지만 props 로 매번 전달해왔는데 한번 global state 를 전달하는 체제를 짜두기만 하면 props 를 매번 전달할 수고가 줄어드는 장점이 느껴졌다.