여기저기서 사용되는 버튼을 하나의 컴포넌트로 만들어서 재사용 하기로 함
현재 버튼이 사용되는 모달창에서 쓰는것까지 포함하여 총 9군데.
그것들을 전부 하나의 공용 컴포넌트화 시켜서 사용한다면 리액트의 장점을 잘 살리는 개발이 될 것이다.
import React from "react";
import styled from "styled-components";
const StBtn = styled.button`
border-radius: 10px;
border-color: transparent;
box-shadow: 3px 3px 3px black;
margin-bottom: 5px;
cursor: pointer;
transition-duration: 0.2s;
&:active {
scale: 0.9;
box-shadow: none;
}
`;
function ReusableButton(props) {
return <StBtn onClick={props.onClick}>{props.children}</StBtn>;
}
export default ReusableButton;
이때 버튼 내부에 표시될 메시지는 사용 장소에 따라 바뀌어야 하므로 props.children
으로 전달 하기로 한다.
버튼이 작동하려면 onClick에 관련된 함수를 달아줘야 할텐데 이것도 props.onClick 으로 받기로 한다. 공용 버튼을 사용하는 컴포넌트에서 onClick prop을 전달해주면 될 일이다.
어느 곳에 적용돼도 문제 없을만한 공용 스타일링만 적용한다.
위 예시에서는 버튼 모서리 라운딩과 눌렸을때 약간의 애니메이션만 추가했다.
<ReusableButton onClick={deleteBtnHndlr}>삭제</ReusableButton>
공용 버튼 컴포넌트를 import 해온 뒤에 사용할 곳에서 위 예시처럼 사용해 주면 된다. 버튼의 크기를 조절 하거나 색을 입히거나 하는건 저 버튼이 사용된 컴포넌트에서 하는 것이 편하다. 현재 프로젝트에서는 모든 컴포넌트에 styled-components를 사용하고 있기 때문에 버튼이 포함되어 있는 컴포넌트에서
const StBtnDiv = styled.div`
display: flex;
justify-content: space-around;
width: 100%;
margin-top: 50px;
🎇button {
padding: 0.8rem;
width: 100px;
cursor: pointer;
}
🎇button:first-of-type {
background-color: #b0e0e688;
}
🎇button:last-of-type {
background-color: #cd32179c;
}
`;
이런 식으로 버튼에 스타일을 입힐 수 있다. 버튼이 두개 이상일 경우에도 CSS에서 제공하는 가상 선택자를 이용하면 어떻게든 할 수 있다.
기존에 window.alert 과 window.confirm 으로 창을 띄우던 것을 직접 만든 모달로 대체 하고자 한다.
이때 모달을 여러개 만드는 것이 아니고 공용 버튼을 만들어서 여러군데서 사용했던 것처럼 모달도 재사용 가능하도록 만들어 보고자 한다.
현재 모달 창이 필요한 곳은 [메시지 삭제 확인창], [수정 오류창], [수정 확인창]이렇게 세군데이다.
import React from "react";
import ReusableButton from "./ReusableButton";
import styled from "styled-components";
// styled-components 를 이용한 스타일은 생략
function ReusableModal(props) {
return (
<>
<StBackdrop onClick={props.onClose} />
<StModalDiv>
<header>
<h2>{props.title}</h2>
</header>
<div>
<p>{props.message}</p>
</div>
<footer>
{props.btnMsg && (
<ReusableButton onClick={props.btnFn}>
{props.btnMsg}
</ReusableButton>
)}
<ReusableButton onClick={props.onClose}>창 닫기</ReusableButton>
</footer>
</StModalDiv>
</>
);
}
export default ReusableModal;
모달의 스타일링은 styled-components를 이용하였으나 코드 전체를 옮기면 너무 길어지기에 생략.
ReusableModal 컴포넌트도 props를 반드시 받아야 하며 해당 props를 토대로 모달의 내용이 채워지도록 구성.
백드롭과 창닫기 버튼에 모두 동일한 onClose 함수를 onClick으로 달아서 [창 닫기] 버튼을 누르거나 그냥 모달 창 밖 아무곳이나 누르면 모달창을 닫을 수 있도록 했다.
버튼은 위에서 만든 공용버튼 컴포넌트를 활용. 이미 버튼 컴포넌트의 onClick 함수나 버튼 내부의 글자를 상황에 맞게 설정할 수 있도록 해 두었기 때문에 모달에도 활용할 수 있다.
모달창의 사용처에 따라 모달창 내부의 버튼 갯수 자체가 달라져야 하는 상황이므로 [창 닫기] 버튼은 기본값으로 둔채로 또 하나의 버튼을 조건부 렌더링 하도록 설정했다. 두번째 버튼이 나타나는 조건은 props로 전달받는 btnMsg가 falsy가 아닐것. 즉 공용 모달 컴포넌트를 사용하는 컴포넌트에서 모달창을 불러올때 버튼 갯수를 직접 조절 가능하게 만들었다. btnMsg prop을 전달하면 버튼이 두개가 생기고 전달하지 않으면 [창 닫기] 버튼 하나만 나타날 것이다. btnMsg prop은 버튼 내부의 글자로도 활용된다.
//before : 모달 미사용
const deleteBtnHndlr = () => {
if (window.confirm("삭제하시겠습니까?")) {
let temp = fanLetters.filter((letter) => letter.id !== matchingLetter.id);
dispatch(setFanLetters(temp));
navigate("/");
}
};
//after : confirm창 대신 모달창 적용
const [modalActivation, setModalActivation] = useState();
const deleteBtnHndlr = () => {
setModalActivation({
title: "삭제 확인",
message: "한번 삭제된 메시지는 복구할 수 없습니다. 삭제하시겠습니까?",
btnMsg: "삭제",
btnFn: onDelete,
});
const onDelete = () => {
let temp = fanLetters.filter((letter) => letter.id !== matchingLetter.id);
dispatch(setFanLetters(temp));
navigate("/");
};
//컴포넌트 함수의 리턴 부분
return (
<>
{modalActivation && (
<ReusableModal
title={modalActivation.title}
message={modalActivation.message}
btnMsg={modalActivation.btnMsg}
btnFn={modalActivation.btnFn}
onClose={onClose}
/>
)}
<StDetailContainer>
//(후략)
설명을 위해 일부 코드를 잘라 왔다. 기존에는 window.confirm 으로 확인창을 띄우던 부분을 모달 컴포넌트로 대체할 것이다.
먼저 모달을 컴포넌트 함수의 return 부분에 삽입한다. 이때 모달창이 조건부로 렌더링 되도록 modalActivation 이라는 state를 선언하고 modalActivation 값을 모달창의 조건으로 설정한다. modalActivation 이 falsy 라면 모달창이 나타나지 않을 것이고 truthy 라면 모달창이 나타날 것이다.
setModalActivation을 사용해서 modalActivation 에 객체를 설정해 준다. 객체인 이유는 위에서도 설명했듯이 모달의 내용을 채우기 위해서 props를 전달해야 하고 이 props를 modalActivation 객체 내부에서 가져오도록 만들었기 때문이다.
객체의 모양을 잠깐 살펴보면
{
title: "삭제 확인",
message: "한번 삭제된 메시지는 복구할 수 없습니다. 삭제하시겠습니까?",
btnMsg: "삭제",
btnFn: onDelete,
}
이런 모양인데 title은 모달의 제목을 전달하고 message는 모달의 내용을 전달하며, btnMsg는 두번째 버튼을 활성화함과 동시에 두번째 버튼에 적힐 글자를 전달한다. 마지막으로 btnFn은 모달내의 두번째 버튼에서 onClick 으로 사용할 함수이다.
위 예시에서는 버튼이 눌렸을때 기존에 사용했던 삭제 로직이 사용되고 navigate("/");
로 이전 페이지로 넘어가기 때문에 모달을 초기화할 필요는 없다. 모달이 사용되지 않는 페이지에서는 모달이 나타나지 않고, 모달을 사용했던 페이지로 다시 돌아와도 이미 state가 리셋되어 있기 때문에 이전 모달이 나타나지 않는다.
하지만 만약 이런 상황이 아니고 모달을 사용했던 페이지에 계속 남아 있는 경우라면 setModalActivation();
를 코드에 넣어서 모달을 닫아줌과 동시에 리셋시켜야 한다.
완성된 공용 모달과 공용 버튼의 모습은 다음과 같다
모달도 재사용 가능한 컴포넌트이고 사진에 나온 버튼들도 전부 재사용 가능한 버튼 컴포넌트이다. 모양이 아주 아름답지는 않지만 그래도 기존에 사용했던 윈도우 confirm 창에 비하면 비교도 할 수 없을 정도로 멋지다고 생각한다.
또한 크기나 위치, 버튼 디자인이나 배치등도 전부 마음대로 바꿀수 있다는 점에서 만족감이 아주 크다.
이번 프로젝트 진행하면서 가장 어렵게 느껴지는 부분이었으나 성공해서 기쁘다.
같은 모달 컴포넌트를 사용하면서도 사용 위치에 따라 다른 내용과 기능으로 사용하게 하기 위해서 조금 복잡하게 느껴지는 방법을 사용했는데 더 쉬운 방법이 있는지 찾아봐야겠다. 어느새 처음에 계획했던 기능적인 부분들은 대부분 구현되어 가는 중인데 이번 프로젝트도 정말 많이 배우게 되는것 같다. 내일은 local storage 활용을 시도해볼 것이며 아이디어가 떠오를 때마다 전체적인 디자인을 개선해 나갈 것이다! :)