[React] alert()대신 customAlert() 만들기 (iOS 대응)

Joowon Jang·2025년 8월 6일

React

목록 보기
17/19

JavaScript 내장함수 alert()

JavaScript에는 기본 alert()라는 내장함수가 있다.
아래와 같이 원하는 시점에 사용자에게 메세지를 전달하기 위해 사용한다.

alert 예시

문제

디자인에 욕심이 있지 않은 이상 alert함수를 사용하면 될텐데, 왜 customAlert를 구현하려 하는지?

iOS환경의 웹뷰에서 JavaScript 내장 함수인 alert() 가 정상적으로 동작하지 않음.

  • 서버에 요청을 보낸 후 돌아오는 응답에 대한 callback으로 alert() 사용 시 동작하지 않음.
  • alert() 함수를 동시에 여러 번 실행하는 등 일부 상황에서 비정상적으로 동작함.

또다른 문제가 있을지 모르지만 일단 내가 찾은 문제점은 이렇게 두가지.

해결

customAlert() 함수를 직접 만들어 alert() 와 유사한 방식으로 사용할 수 있도록 함.
→ 기존의 코드를 거의 변경하지 않고도 문제 해결 가능.
alert()와 동일한 사용법으로 즉시 적용가능.

구현 (Next.js / React)

  • Next.js 환경에서도 문제없이 사용하기 위해 Client Side인지 체크 (SSR 방지 주석 부분)

  • 알림창을 띄우고, 확인 버튼을 클릭하기 전까지 그 다음 줄에 오는 코드를 실행하지 않도록 멈춰주기 위해 Promise를 return하도록 함. (Promise를 return하려면 작업 성공, 실패에 따른 resolve를 반환해주어야 하기 때문에 ‘알림창 띄우기’ 동작을 실행하는 역할의 implement 함수의 인수로 resolve를 전달)

    • 해당 resolve는 아무런 내용 없음. Promise를 사용해 다음 작업을 실행하지 않고 멈춰있기 위한 수단일 뿐.

자세한 설명은 아래 설명 및 주석 참고

customAlert.ts

customAlert.ts에서 AlertProvider.tsx의 ‘알림창 띄우기’ 동작을 실행하는 함수를 받아 implement라는 변수에 할당하여 실행.

// 코드에서 직접 호출할 customAlert() 함수를 정의
/*
React의 동작 흐름에 따라 AlertProvider가 예측 가능한 동작을 하도록
setAlertImplementation 함수를 AlertProvider에서 실행해
'알림창 띄우기' 동작(함수)을 implement에 할당
*/

type AlertFunction = (msg: React.ReactNode, resolve: () => void) => void;

let implement: AlertFunction | null = null;

// AlertProvider에서 아래의 함수를 실행하여 implement 변수에 '알림창 띄우기' 동작을 하는 함수 할당
export const setAlertImplementation = (fn: AlertFunction) => {
  implement = fn;
};

// '알림창 띄우기' 동작을 하는 함수 실행 (알림창에 들어갈 메세지는 이 함수에서 받음)
// customAlert("알림메세지"); 의 형태로 실행
export const customAlert = (msg: React.ReactNode): Promise<void> => {
  if (typeof window === 'undefined' || !implement) return Promise.resolve(); // SSR 방지

  return new Promise((resolve) => {
    implement?.(msg, resolve);
  });
};

/* ---------------------------------- 사용예시 ---------------------------------- */

/*
import { customAlert } from '@/lib/customAlert';

// ...

if(~~) {
	customAlert("async함수 바깥이라 await 사용이 불가능하다면 then으로 처리").then(() => {
		router.push("권한이 없습니다.") // redirect
	});
}

<button
  type="button"
  onClick={async () => await customAlert(
    '여기에 메시지를 넣으면 alert() 대신 사용가능. 비동기 함수에서 await를 사용하면 다음 동작을 블로킹 할 수 있음.'
  )}>
  test
</button>

<button
  type="button"
  onClick={() => customAlert(
    <div>
      <p>이렇게 JSX도 사용가능</p>
    </div>
  )}
  test
</button>

// ...
*/

이렇게 처리하는 이유는, React는 선언형 프로그래밍 구조를 따르는데,
알림창을 띄우는 JavaScript 내장 alert() 함수는 명령형 프로그래밍 구조이기 때문에,
alert()처럼 동작하는 customAlert() 함수가 React의 상태를 변경하는 코드와 한 파일에 섞이게 되면 모듈 간 결합도가 높아져 유지보수가 힘들기 때문.
→ 따라서 customAlert()별도의 라이브러리처럼 분리하여 lib 폴더에 작성하고, React 내부의 상태를 변경하는 ‘알림창 띄우기’를 실행할 함수는 React의 AlertProvider.tsx에서 받아서 실행하도록 함(AlertProvider가 customAlert 함수의 동작을 예측 가능).

  • 선언형 프로그래밍: 상태(state)가 변경됨에 따라 UI가 변경된다는 흐름으로, 상태 관리에만 집중하는 구조.
    UI를 변경하는 과정은 React 라이브러리에 맡기고 개발자는 상태를 변경하는 데에 집중함.
    DOM 조작과 같은 부분을 직접 수정하지 않기 때문에 코드가 간결하고 유지보수에 용이함.
  • 명령형 프로그래밍: UI 변경 등의 모든 동작을 개발자가 직접 실행하여 처리하는 구조.
    alert()는 특정 시점에 알림창을 띄우는 행위를 개발자가 모두 지정하는 형태이기 때문에 명령형임.
    모든 로직이 하나의 코드에 뭉쳐있어 코드 파악과 유지보수가 어려움.
    ex) jQuery 등

layout.tsx

// 최상위 layout.tsx에 AlertProvider를 추가하여 앱의 모든 곳(ClientSide한정)에서 customAlert() 함수 사용 가능
import { AlertProvider } from './providers/AlertProvider';
// ...

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
		{/* ... */}
      <body>
        <AlertProvider /> {/* customAlert() 함수 제공 */}
        {/* ... */}
      </body>
    {/* ... */}
  );
}

AlertProvider.tsx

'use client'; // Client Side

import { cs } from '@/app/components/styles';
import { setAlertImplementation } from '@/lib/customAlert';
import { ReactNode, useEffect, useState } from 'react';

export const AlertProvider = () => {
  const [message, setMessage] = useState<ReactNode | null>(null);
  const [onClose, setOnClose] = useState<(() => void) | null>(null); 
  /*
  await 사용 시. 즉, Promise를 반환하는 상황에는 알림창을 닫는 동작과 함께
  resolve를 반환해줘야 하기 때문에, resolve(() => void); 이렇게 실행해야 하지만,
  resolve는 Promise executor 안에만 존재하기 때문에
  AlertProvider에서는 customAlert.ts에 있는 implement 함수의 resolve를 직접 설정이 불가능하고,
  변수를 통해 전달해야 하는데,
  일반 변수에 () => void 를 할당하게 되면 리렌더링이 발생하는 경우
  함수 참조를 잃게 되므로 정상 실행이 안될 수 있음.
  그래서 useState나 useRef로 안전하게 관리.
  (React의 useState, useRef는 리렌더링 시에도 값을 유지함)
	*/

	// 
  const showAlert = (msg: ReactNode, resolve: () => void) => {
    setMessage(msg);
    setOnClose(() => resolve);
  };

  const closeAlert = () => {
    setMessage(null);
    if (onClose) {
      onClose(); // Promise resolve
      setOnClose(null);
    }
  };

  useEffect(() => {
    setAlertImplementation(showAlert); // customModal.ts의 implement 변수에 '모달창 띄우기' 함수를 할당
  }, []);

  if (!message) return null;

  return (
	  {/* 커스텀 alert창 JSX */}
    <div
	  role="dialog"
      className="fixed top-[50%] left-[50%] bg-black text-white py-4 px-6 rounded shadow z-50 max-w-sm -translate-x-1/2 -translate-y-1/2 flex flex-col z-[9999]"
    >
      <div className="mb-2">{message}</div>
      <button
        onClick={closeAlert}
        className="mt-2 bg-white text-black px-3 py-1 rounded hover:bg-gray-100"
        >
        확인
      </button>
    </div>
  );
};

결과

customAlert 예시
예쁘게 꾸미면 더 좋을듯!

다음 포스팅은 customAlert()를 확장한 customPrompt(), customConfirm() 구현과정 및 유용하게 사용할 수 있는 상황에 대해 정리하겠습니다!

profile
깊이 공부하는 웹개발자

0개의 댓글