서론

앞에서 Todo 앱을 만들때처럼 Modal 을 만들기 위해 redux 를 사용해보자. redux 를 내 것으로 만들기 위해 이런저런 코드들을 직접 redux 로 구현해보고자 한다.

요구사항 정의하기

  • 모달을 켜는 버튼과 모달을 닫는 버튼
  • 채팅창에 글을 유저가 타이핑하기 위한 입력창
  • 현재 입력한 글자수와 입력 가능한 최대 글자수를 나타내는 label
  • 텍스트를 감싸는 말풍선 영역
  • 타이핑한 내용을 전송하기 위한 요청 버튼

상태값 디자인하기

모달에 필요한 것은 전체 대화 item 리스트와 모달을 나타낼지 여부이다.

각 대화 item 은 다음의 정보를 저장해야 한다.

  • 채팅을 입력한 주체가 system / user 중 어느쪽인지를 나타내는 값
  • 채팅 내용

또한 모달을 나타낼지 여부를 저장해야 한다.

  • 모달이 열렸는가?

이처럼 채팅에 대한 상태와 app state 모달을 띄우는지 여부를 나타내는 상태 UI state 로 나뉜다.

상태 구조 디자인하기

각 채팅 아이템은 다음의 필드가 필요할 것이다.

  • id: 채팅을 날린 시각
  • role: "SYSTEM" / "USER"
  • text: 각 주체가 room 에 전송한 텍스트

모달이 열렸는지 여부는 다음 열거값들로 설명할 수 있다.

  • 모달 상태 : Opened , Closed
const initialState = {
	chats: [
      { id: 168568774, role: "SYSTEM", text: '무엇을 도와드릴까요?'},
    ],
    modal: {
    	status: 'Opened'
    }
}

Action 디자인하기

  • GPT 버튼을 클릭시 모달 띄우기
  • 모달의 닫기 버튼 클릭시 모달 닫기
  • 유저가 입력한 텍스트를 기반으로 채팅 전송하기
  • GPT 가 유저의 채팅을 보고 응답한 텍스트를 채팅에 전송하기

위의 액션 리스트를 type 과 payload 형식으로 전환한 형식을 생각해봤다.

  • { type: 'modals/modalOpened' }
  • { type: 'modals/modalClosed' }
  • { type: 'chats/userChatPosted', payload: text }
  • { type: 'chats/systemChatPosted', payload: text }

Redux 환경 준비하기

sudo npm i @reduxjs/toolkit
sudo npm i redux
sudo npm i react-redux

toolkit 과 redux 를 설치해줬다.

그리고 toolkit 의 createSlice 를 사용하고자 한다.

Reducer 작성하기

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 를 매번 전달할 수고가 줄어드는 장점이 느껴졌다.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글