너저분한 모달 상태관리,,,
모달의 UI는 한정되어있는데 페이지마다 모달의 상태관리를 저렇게 state로 무지성 난사로 쓰다보니 불필요한 재렌더링도 그렇고, 불필요한 코드 중복성이 늘어나 리팩토링의 필요성을 느꼈다.
멘토님께서 리액트 포탈로 관리해보기 전, useContext로 해보자고 하셔서 useContext로 리팩토링을 우선하였다.
보여줄 모달을 전역으로 관리하게 되면 다음과 같은 장점이 있다.
1. 재사용성: 같은 모달을 쉽게 사용하게 하여 중복성을 제거한다.
2. 통제 용이성: 어느 위치에서든 모달의 상태를 통제할 수 있어, props drilling을 피할 수 있다.
context/ModalContext.jsx 생성
import React, { createContext, useState } from "react";
export const ModalContext = createContext();
export function ModalContextProvider({ children }) {
const [modalComponent, setModalComponent] = useState(null);
return (
// eslint-disable-next-line react/jsx-no-constructed-context-values
<ModalContext.Provider value={{ setModalComponent }}>
{children}
{modalComponent}
</ModalContext.Provider>
);
}
export default ModalContext;
ModalContext는 모달 관련 정보를 하위 컴포넌트에 전달하는 데 사용된다.
ModalContextProvider은 이 Context를 제공하는데 사용되는 컴포넌트다.
ModalContextProvider 컴포넌트에서 useState를 이용해 modalComponent 상태를 생성하고 초깃값은 null로 한다. 이를 value값으로 넣어주어 하위 컴포넌트에게 제공한다.
하위 컴포넌트는 제공받은 setModalComponent를 통해 모달 컴포넌트를 열거나 닫을 수 있다.
이 ModalContextProvider을 defaultLayout 또는 App.js에서 감싸준다.
return(
<ModalContextProvider>
...
</ModalContextProvider>
모달을 띄울 컴포넌트에 적용을 해주면 끝!
const { setModalComponent } = useContext(ModalContext);
const handleEditReq = async () => {
try {
const response = await editIssueHistory(issueId, contents);
if (response === 200) {
setModalComponent(
<OkOnlyModal
message="수정되었습니다."
handleOnClick={() => {
setModalComponent(null);
issueListRerender(); // API재호출
}}
/>,
);
}
} catch (error) {
console.error("이슈 수정 처리 중 오류 발생", error);
setModalComponent(
<OkOnlyModal
message="수정 요청이 실패했습니다."
handleOnClick={() => {
setModalComponent(null);
}}
/>,
);
}
};
코드처럼 띄워줄 모달, 이 코드에선 'okOnlyModal'을 setModalComponent에 넣어주어 모달을 띄울 수 있도록 하였고,
모달을 닫을 때는 setModalComponent(null)로 닫아주도록 했다.
추가로 배운 재미진 리팩토링
1. 프레젠테이션과 컨테이너 부분을 나눌 때, 컨테이너에서 프레젠테이션으로 props를 넘기는 부분에서 객체에 콜백을 담아보내는 참신한 방식.
import React, { useState } from "react";
import { ReactComponent as ThreeDots } from "assets/icon/threeDots.svg";
import { Button } from "styles/styled";
import styled from "styled-components";
import useOutsideClick from "hooks/useOutsideClick";
function TreeDotsSelector({ options }) {
const [isDropdownOpen, setIsDropdownOpend] = useState(false);
const handleClickOutside = () => {
setIsDropdownOpend(false);
};
const ref = useOutsideClick(handleClickOutside);
return (
<div>
<Button
width="2.5rem"
padding="0rem 0rem 0rem 0rem"
height="1.5rem"
onClick={(event) => {
event.stopPropagation();
setIsDropdownOpend(true);
}}
>
<ThreeDots style={{ width: "16px", height: "14px" }} />{" "}
</Button>
{isDropdownOpen && (
<DropdownBox ref={ref}>
{options.map((option) => (
<Option key={option.name} onClick={option.onClickHandler}>
{option.name}
</Option>
))}
</DropdownBox>
)}
</div>
);
}
여기는 프레젠테이션 부분인데, dropbox에 들어갈 옵션을 컨테이너에서 props로 받아 띄워준다.
이때 option을 클릭할 때 동작할 콜백함수를 'onClickHandler' key값으로 받고 있다.
<ThreeDotsSelector
options={[
{
name: "수정",
onClickHandler: () => {
setModalComponent(
<IssueEditFormModal
issueNum={index + 1}
issueId={issue.id}
prevContent={issue.content}
issueListRerender={fetchIssueHistory}
/>,
);
},
},
{
name: "삭제",
onClickHandler: () => {
setModalComponent(
<QuestionModal
message={`${
index + 1
}번 이슈히스토리를 삭제하시겠습니까?`}
handleOnClick={() => {
handleDeleteReq(issue.id);
}}
setShowModal={() => {
setModalComponent(null);
}}
okButtonMessage="삭제"
/>,
);
},
},
]}
/>
드롭박스의 컨테이너 부분으로 옵션에 들어갈 부분을 options라는 배열로 props를 보내준다.
2. 처음에는 데이터를 수정, 삭제 후에 window.location.reload를 통해 강제 새로고침을 시켜줬는데 이건 UX적으로 너무 별로다. 따라서 원하는 동작을 수행한 후에 바뀌는 데이터부분만 api get요청을 다시 보낼 수 있도록 콜백함수로 props를 넘겨주었다.
이 다음으로는 리액트 포탈을 이용하여 프로젝트 전체적으로 모달 관리를 리팩토링해볼 예정이다!
'TIL' 카테고리의 다른 글
리액트 다루는 기술 스터디 2주차 정리 - 3,4장 & 제어, 비제어 컴포넌트 (0) | 2024.01.31 |
---|---|
리액트 다루는 기술 1주차 스터디 정리 (0) | 2024.01.16 |
[TIL] 12/5 - 리액트, 기업 프로젝트 기록 (1) | 2023.12.05 |
11/8 next.js 프로젝트 진행 - radix progress bar (0) | 2023.11.08 |