본문 바로가기
React

[React]페이지 이탈 시 확인 모달창 띄우기

by jyee 2024. 9. 13.
728x90
반응형

 

참고한 블로그

https://choisuhyeok.tistory.com/m/140 

 

[React] React Router로 페이지 이동 시 폼 데이터 손실 방지하기: usePrompt와 beforeunload 활용법

폼을 작성하는 페이지에서 다른 페이지로 이동하려는 시도 시, 이동 전에 현재 작성된 내용이 손실될 수 있다는 경고 메시지가 나타나는 UI는 많은 사용자들이 익히 본 경험이

choisuhyeok.tistory.com

 

❌❌❌❌제대로 동작되지 않은 코드입니다 참고하지 말기❌❌❌❌

내가 작성한 코드

communitywrite 

import React, { useState, useRef, useEffect } from "react";
import {
  CustomHeader,
  Button,
  Selectbox,
  TemporarySave,
} from "../components/index";
import { FaTimes } from "react-icons/fa";
import circleImg from "../components/images/growclub-img/circleImg.png";
import { useMediaQuery } from "react-responsive";
import { useNavigate, useBlocker } from "react-router-dom";
interface FileState {
  url: string;
  type: "image" | "video";
}

const CommunityWrite: React.FC = () => {
  const [selectedCategory, setSelectedCategory] = useState("");
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [tempSave, setTempSave] = useState(false);
  const [isBlocking, setIsBlocking] = useState(true);
  const [isNavigating, setIsNavigating] = useState<string | null>(null);
  const [file, setFile] = useState<FileState[]>([]);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const navigate = useNavigate(); 

  const isDesktop = useMediaQuery({ query: "(min-width: 1024px)" });

  const categoryOption = [
    { value: "그롱챌린지", label: "그롱챌린지" },
    { value: "자유게시판", label: "자유게시판" },
  ];

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files && event.target.files.length > 0) {
      Array.from(event.target.files).forEach((selectedFile) => {
        const fileType = selectedFile.type.includes("image")
          ? "image"
          : "video";
        addFile({
          url: URL.createObjectURL(selectedFile),
          type: fileType,
        });
      });
    }
  };

  const addFile = (newFile: FileState) => {
    setFile((prevFiles) => [...prevFiles, newFile]);
  };

  const removeFile = (index: number) => {
    setFile((prevFiles) => prevFiles.filter((_, i) => i !== index));
  };

  const handleSubmit = () => {
    if (file.length === 0) {
      alert("파일을 선택해주세요.");
      return;
    }
  };

  const handleTempsave = () => {
    setTempSave(true);
  };


  const usePrompt = (when: boolean) => {
    const blocker = useBlocker(({ currentLocation, nextLocation }) => {
      if (when && currentLocation.pathname !== nextLocation.pathname) {
        setIsNavigating(nextLocation.pathname);
        return true;
      }
      return false;
    });

    useEffect(() => {
      if (blocker.state === "blocked") {
        setTempSave(true);
      }
    }, [blocker.state]);

    const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
      if (when) {
        event.preventDefault();
        // event.returnValue = "";
      }
    };

    useEffect(() => {
      window.addEventListener("beforeunload", beforeUnloadHandler);
      return () => {
        window.removeEventListener("beforeunload", beforeUnloadHandler);
      };
    }, [when]);
  };

  const handleContinueWriting = () => {
    setTempSave(false);
    setIsNavigating(null);
  };

  const handleLaterWriting = () => {
    setIsBlocking(false);
    if (isNavigating) {
      navigate(isNavigating);
    } else {
      navigate(-1);
    }
  };

  // PC 버전일 때만 usePrompt 훅을 실행
  usePrompt(isDesktop && isBlocking);

  return (
    <div className="p-4 space-y-10 lg:p-11 lg:w-2/3 lg:mx-auto">
      <div className="lg:hidden">
        <CustomHeader headerName="글쓰기" onClick={handleTempsave} />
      </div>

      <div className="text-lg font-bold">
        <p className="mb-2 flex gap-1">
          게시판 선택
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <Selectbox
          options={categoryOption}
          onChange={(value) => setSelectedCategory(value)}
          value={selectedCategory}
          className="w-full text-sm border border-gray-400 rounded font-normal"
          placeholder="게시판을 선택해주세요"
        />
      </div>

      <div className="font-bold">
        <p className="text-lg mb-2 flex gap-1">
          제목
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <input
          type="text"
          className="w-full text-sm placeholder-mypagegray font-normal px-4 py-2 border border-gray-400 rounded"
          placeholder="제목을 입력해주세요"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>

      <div className="text-lg font-bold">
        <p className="mb-2 flex gap-1">
          내용
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <textarea
          className="w-full text-sm placeholder-mypagegray font-normal px-4 py-3 h-60 border border-gray-400 rounded"
          placeholder="내용을 입력해주세요"
          value={content}
          onChange={(e) => setContent(e.target.value)}
        />
      </div>

      <input
        type="file"
        ref={fileInputRef}
        onChange={handleFileChange}
        className="hidden"
        accept="image/*, video/*"
        multiple
      />

      <div className="flex flex-wrap gap-2 mt-4">
        {file.map((file, index) => (
          <div key={index} className="relative">
            {file.type === "image" ? (
              <img
                src={file.url}
                alt={`Uploaded image ${index}`}
                className="w-24 h-24 object-cover"
              />
            ) : (
              <video
                src={file.url}
                controls
                className="w-24 h-24 object-cover"
              />
            )}
            <button
              onClick={() => removeFile(index)}
              className="absolute top-1 right-1 bg-gray-400 text-white rounded-full p-1"
            >
              <FaTimes />
            </button>
          </div>
        ))}
      </div>

      {tempSave && (
        <div className="fixed inset-0 flex items-center justify-center z-50">
          <div className="fixed inset-0 bg-black opacity-40"></div>
          <TemporarySave
            onClose={handleContinueWriting}
            onLater={handleLaterWriting}
            textHead="나중에 이어서 쓰시겠어요?"
            textcontent={`혹시 몰라, 지금까지 쓴 내용을 \n 임시 저장해두었어요.`}
            cancleTxt="취소"
            okTxt="나중에 쓰기"
          />
        </div>
      )}

      <Button
        className="w-full border text-base text-black bg-growbg border-paybutton mt-2"
        onClick={() => fileInputRef.current?.click()}
      >
        이미지 업로드
      </Button>

      <Button className="w-full text-base mt-8" onClick={handleSubmit}>
        등록하기
      </Button>
    </div>
  );
};

export default CommunityWrite;

 

 

페이지 이탈 시 띄울 모달

import React from "react";
import { Button } from "../shadcn/button";
import CautionImg from "../images/mypage-img/caution.png";
import { useMediaQuery } from "react-responsive";

interface TemporarySaveProps {
  onClose: () => void;
  onLater: () => void;
  textHead: string;
  textcontent: string;
  cancleTxt: string;
  okTxt: string;
}

const TemporarySave: React.FC<TemporarySaveProps> = ({ onClose, onLater, textHead, textcontent, okTxt, cancleTxt }) => {
  const isDesktop = useMediaQuery({ query:"(min-width: 1024px)"})

  return (
    <div className={`fixed bg-white p-4 ${isDesktop ? " rounded-2xl w-2/6 ": "inset-x-0 bottom-0 rounded-t-xl" }`}>
      <div className="mt-4 flex flex-col gap-4">
        <div className="flex flex-col items-center gap-1">
          <div className="relative w-24 h-30 object-contain">
            <img
              src={CautionImg}
              alt="CautionImg"
              className="w-full h-full object-cover"
            />
          </div>
          <p className="pt-2 font-extrabold text-lg">
            {textHead}
          </p>
          <div className="text-gray-500 text-center text-sm pb-4 whitespace-pre-line">
            {textcontent}
          </div>
        </div>
        <div className="flex justify-center gap-4">
          <Button
            className="bg-white text-black border border-black rounded-lg w-full"
            onClick={onClose}
          >
            {cancleTxt}
          </Button>
          <Button className="bg-black w-full" onClick={onLater}>
            {okTxt}
          </Button>
        </div>
      </div>
    </div>
  );
};

export default TemporarySave;

 

📛 문제였던 부분 분석하기 

1.  pc버전에서 모달창이 뜨고 나중에 쓰기 버튼을 누르면 2번을 클릭해야 만 페이지 이동이 됨

2.  취소버튼을 누르면 모달창이 닫기면서 정상작동 되는 듯 하지만 다시 페이지 이동을 하기 위해 클릭을 하면 모달창이 뜨지도 않고 페이지 이동도 되지 않음 

 

솔직히 말하면 다른 사람이 쓴 코드 제대로 이해 안한 상태에서 그대로 비슷하게 짜서 결국 어떤 로직이 문제인지 몰랐던거 같다. 

 

결국 해결 못하고 사수님께 넘겼었다. 

사수님이 해결하셨고 참고하셨다는 그대로 코드 작성하신 documentation을 주셔서 봤는데  useEffect이 필요가 없고 내가 쓴 코드 로직에는 너무 복잡한 느낌이였다 

 

https://reactrouter.com/en/main/hooks/use-blocker

 

useBlocker v6.26.2 | React Router

 

reactrouter.com

 

 

⭕️⭕️⭕️⭕️해결된 코드 ⭕️⭕️⭕️⭕️

import React, { useState, useRef, useEffect } from "react";
import {
  CustomHeader,
  Button,
  Selectbox,
  TemporarySave,
} from "../components/index";
import { FaTimes } from "react-icons/fa";
import circleImg from "../components/images/growclub-img/circleImg.png";
import { useMediaQuery } from "react-responsive";
import { useNavigate, useBlocker } from "react-router-dom";
interface FileState {
  url: string;
  type: "image" | "video";
}

const CommunityWrite: React.FC = () => {
  const [selectedCategory, setSelectedCategory] = useState("");
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const [file, setFile] = useState<FileState[]>([]);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const categoryOption = [
    { value: "그롱챌린지", label: "그롱챌린지" },
    { value: "자유게시판", label: "자유게시판" },
  ];

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files && event.target.files.length > 0) {
      Array.from(event.target.files).forEach((selectedFile) => {
        const fileType = selectedFile.type.includes("image")
          ? "image"
          : "video";
        addFile({
          url: URL.createObjectURL(selectedFile),
          type: fileType,
        });
      });
    }
  };

  const addFile = (newFile: FileState) => {
    setFile((prevFiles) => [...prevFiles, newFile]);
  };

  const removeFile = (index: number) => {
    setFile((prevFiles) => prevFiles.filter((_, i) => i !== index));
  };

  const handleSubmit = () => {
    if (file.length === 0) {
      alert("파일을 선택해주세요.");
      return;
    }
  };

  const blocker = useBlocker(({ currentLocation, nextLocation }) => {
    return currentLocation.pathname !== nextLocation.pathname;
  });

  return (
    <div className="p-4 space-y-10 lg:p-11 lg:w-2/3 lg:mx-auto">
      <div className="lg:hidden">
        <CustomHeader headerName="글쓰기" />
      </div>

      <div className="text-lg font-bold">
        <p className="mb-2 flex gap-1">
          게시판 선택
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <Selectbox
          options={categoryOption}
          onChange={(value) => setSelectedCategory(value)}
          value={selectedCategory}
          className="w-full text-sm border border-gray-400 rounded font-normal"
          placeholder="게시판을 선택해주세요"
        />
      </div>

      <div className="font-bold">
        <p className="text-lg mb-2 flex gap-1">
          제목
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <input
          type="text"
          className="w-full text-sm placeholder-mypagegray font-normal px-4 py-2 border border-gray-400 rounded"
          placeholder="제목을 입력해주세요"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>

      <div className="text-lg font-bold">
        <p className="mb-2 flex gap-1">
          내용
          <img src={circleImg} className="w-1.3 h-1" />
        </p>
        <textarea
          className="w-full text-sm placeholder-mypagegray font-normal px-4 py-3 h-60 border border-gray-400 rounded"
          placeholder="내용을 입력해주세요"
          value={content}
          onChange={(e) => setContent(e.target.value)}
        />
      </div>

      <input
        type="file"
        ref={fileInputRef}
        onChange={handleFileChange}
        className="hidden"
        accept="image/*, video/*"
        multiple
      />

      <div className="flex flex-wrap gap-2 mt-4">
        {file.map((file, index) => (
          <div key={index} className="relative">
            {file.type === "image" ? (
              <img
                src={file.url}
                alt={`Uploaded image ${index}`}
                className="w-24 h-24 object-cover"
              />
            ) : (
              <video
                src={file.url}
                controls
                className="w-24 h-24 object-cover"
              />
            )}
            <button
              onClick={() => removeFile(index)}
              className="absolute top-1 right-1 bg-gray-400 text-white rounded-full p-1"
            >
              <FaTimes />
            </button>
          </div>
        ))}
      </div>

      {blocker.state === "blocked" ? (
        <div className="fixed inset-0 flex items-center justify-center z-50">
          <div className="fixed inset-0 bg-black opacity-40"></div>
          <TemporarySave
            onClose={() => blocker.reset()}
            onLater={() => blocker.proceed()}
          />
        </div>
      ) : null}

      <Button
        className="w-full border text-base text-black bg-growbg border-paybutton mt-2"
        onClick={() => fileInputRef.current?.click()}
      >
        이미지 업로드
      </Button>

      <Button className="w-full text-base mt-8" onClick={handleSubmit}>
        등록하기
      </Button>
    </div>
  );
};

export default CommunityWrite;
728x90
반응형