프론트엔드갈아엎기 시리즈

sweetalert2 호출 19 곳 통합 + 환경변수 단일화 + 코스 저장 가드 버그 수정 — POP-SPOT v2.6 프론트 결합도 정리

김동현
··4분 읽기

/shop 라우트 폴더 삭제 · 환경변수 src/lib/env.ts 로 단일화 · Kakao 지도 스크립트 undefined 키 가드 · 메인의 sweetalert2 직접 호출 19 곳을 공용 notify 로 · 장소 추가 모달 분리 · 취소 눌러도 저장되던 버그 수정.

메인 페이지가 sweetalert2 를 19 곳에서 직접 호출, 환경변수는 5 곳에서 따로따로 읽음, /shop 라우트는 폐기했다면서도 단순 리다이렉트만 남아 있음. 코스 저장 가드 버그도 같이 잡은 한 라운드.

이 글에서 다루는 것

  • /shop 이 왜 더 이상 필요 없는지와 폴더까지 지워야 하는 이유
  • 환경변수를 src/lib/env.ts 한 곳에서만 참조하게 한 이유
  • Kakao 지도 스크립트가 키 없이도 로드되던 문제 (appkey=undefined) 가드
  • sweetalert2 19 곳 직접 호출 → 공용 알림 함수 (src/lib/notify.ts) 로 교체
  • AddPlaceModal 분리와 접근성 개선
  • 코스 저장에서 "취소" 눌러도 저장이 실행되던 가드 버그 수정
  • 모르는 단어 한 줄로

    용어한 줄 설명
    sweetalert2"alert·confirm·prompt 이쁜 버전" 레퍼런스 라이브러리
    결합도 (coupling)"이게 바뀌면 저게 같이 바뀌어야 하는 정도". 낮을수록 좋음
    aria-modal스크린 리더·키보드 사용자에게 "지금 모달이야" 를 알려주는 속성
    guard 명령조건을 먼저 검사하고, 맞으면 다음을 진행하거나 멈추는 패턴

    무엇이 바뀌었나

    항목v2.5v2.6
    /shop 라우트폐기됐지만 파일이 남음폴더 통째 삭제
    환경변수5 파일에서 따로따로src/lib/env.ts 한 파일에서만 관리
    Kakao 지도 스크립트키 비어있어도 <code>appkey=undefined</code> 로 로드키 있을 때만 삽입
    page.tsx 알림sweetalert2 를 19 곳에서 직접 호출notify.ts 공용 함수로 교체 (sweetalert2 직접 호출 0 개)
    코스 저장 가드취소를 눌러도 저장이 그대로 실행됨확인 결과 대기 후 외부 함수에서 분기
    모달 분리"장소 추가하기" 27 줄이 메인 파일에 박혀있음AddPlaceModal.tsx 분리, aria-modal + focus trap

    왜 이렇게 했음

    공용 래퍼 (notify) — sweetalert2 의 기본 스타일은 POP-SPOT 의 남은 색조·파스텔 톤과 잘 맞지 않음. 그래서 공용 함수 하나에서 기본 아이콘·채도·버튼 텍스트 세팅을 한 곳에 박아둔다. 나중에 sweetalert2 이 아닌 다른 라이브러리로 갈아타도 19 곳을 고칠 필요 없음.

    취소 가드 — sweetalert2 의 confirm 은 비동기 (Promise) 다. 역사적으로는 callback 으로 쓰이며 코드 안에서는 confirm 이 반환되자마자 save() 를 호출하는 패턴이 남아 있었음. 결과: 취소를 눌러도 save 가 실행. await 로 결과 받아 if (result.isConfirmed) save() 로 변경.

    접근성 — 메인 안에 27 줄로 들어있는 모달은 focus trap (Tab 눌렀을 때 모달 안에서만 포커스 이동) · Esc 로 닫혀서 이걸 구현하기 어렵다. 독립 컴포넌트로 쪼개면 aria-modal · inert 속성을 깔끔하게 적용할 수 있다.


    코드로는 어떻게 (필요한 부분만)

    공용 notify.

    파일: popspot-frontend/src/lib/notify.ts (v2.6 신규 생성)

    typescript
    // v2.6 — src/lib/notify.ts. 공용 알림 래퍼
    import Swal from 'sweetalert2';
    // 위: sweetalert2 는 이 파일 하나에서만 import. 19곳에서 이제 직접 import 안 함
    
    const base = Swal.mixin({
      // 위: mixin 은 공통 옵션 설정. 호출마다 동일 옵션을 반복 적용하고 싶을 때 쓰는 sweetalert 패턴
      buttonsStyling: false,
      // 위: sweetalert 의 기본 버튼 스타일 비활성화.
      //      false 로 안 두면 sweetalert 자체 파란/빨간 버튼이 떠서 POP-SPOT 톤과 충돌
      customClass: {
        confirmButton: 'bg-zinc-900 text-white rounded-lg px-4 py-2',
        // 위: 확인 버튼 Tailwind 클래스 — 진한 회색 배경 + 흰 글자 (Primary 느낌)
        cancelButton:  'bg-zinc-200 rounded-lg px-4 py-2 mx-2',
        // 위: 취소 버튼 — 연한 회색 배경 (Secondary). 좌우 mx-2 로 확인 버튼과 간격
      },
    });
    
    export const notify = {
      // 위: 19곳에서 동일한 단순 인터페이스로 호출할 수 있도록 객체로 묶음
      success: (msg: string) => base.fire({ icon: 'success', text: msg }),
      // 위: 성공 토스트. 초록 체크 아이콘 + msg 메시지
      error:   (msg: string) => base.fire({ icon: 'error',   text: msg }),
      // 위: 에러 토스트. 빨간 X 아이콘 + msg 메시지
      confirm: (msg: string) =>
        base.fire({ icon: 'question', text: msg,
                    showCancelButton: true }).then(r => r.isConfirmed),
      // 위: 예/아니오 물음. Promise 끝에 .then(r => r.isConfirmed) 를 붙여
      //      "확인이 눌렸는지" 의 boolean 만 반환 — 호출 쪽에서 쓰기 쉽도록
    };

    취소 가드 수정.

    파일: popspot-frontend/src/app/page.tsx 의 코스 저장 핸들러

    typescript
    // v2.5 까지 — confirm 답변을 기다리지 않고 바로 저장
    Swal.fire({ text: '저장할까요?' });
    // 위: Swal.fire 는 Promise 를 리턴 — await 없이 그냥 쓰면
    //      다음 줄의 save() 호출이 바로 일어남 (팝업이 떠 있으는데도)
    //      비동기 흐름을 구분 안 하면 이런 버그 자주 생김
    save();
    // 위: 팝업이 아직 떠있는데도 저장 실행. 취소 눌러도 동일하게 저장됨 (가드 의미 없음)

    파일: 같은 app/page.tsx (v2.6 교체 후)

    typescript
    // v2.6 — await 로 사용자 선택 기다리고 확인 양자일 때만 저장
    if (await notify.confirm('저장할까요?')) {
      // 위: notify.confirm 은 위에서 만든 래퍼. Promise<boolean> 리턴
      //      await 로 우아하게 해결 → true 면 if 내부 실행 계속
      //      하위 원항도 async 함수여야 await 키워드 사용 가능
      save();
      // 위: 이제는 동의했을 때만 호출됨 — 장·별 일·견으는 안·전 조건·부 주 단·단 클 괔 을 명·명·되 며 하 는 숈 은 ·· ·· · · · ··· ·· ··· ·· ·· ··
    }

    Kakao 지도 스크립트 키 가드.

    파일: popspot-frontend/src/app/layout.tsx 또는 지도를 쓰는 페이지의 <Script /> 부분

    typescript
    // v2.5 까지 — KEY 가 undefined 여도 그대로 URL 제조
    <Script src={`//dapi.kakao.com/.../sdk.js?appkey=${KEY}&autoload=false`} />
    // 위: 템플릿 리터럴 안 ${KEY} 가 undefined 이면
    //      문자열로 "undefined" 로 변환되어 ?appkey=undefined 가 완성됨
    //      Kakao SDK 가 401/403 으로 실패 + 콘솔에 대량의 에러
    //      개발 중에는 환경변수가 빠진 상황이 자주 생김·클라이언트에 에러 노출 안 되도록 차단

    파일: 같은 layout.tsx (v2.6 수정 후)

    typescript
    // v2.6 — KEY 가 있을 때만 <Script> 자체를 렌더링
    {KEY && (
      // 위: 논리·AND 명제 — KEY 가 truthy 일 때만 아래 JSX 소환
      //      KEY=undefined / KEY='' 경우 false 는 React 가 아무 것도 렌더링 안 함
      <Script src={`//dapi.kakao.com/.../sdk.js?appkey=${KEY}&autoload=false`} />
      // 위: KEY 가 있을 때만 도달하므로 appkey 는 항상 유효한 값
    )}

    핵심 파일: popspot-frontend/src/lib/env.ts, popspot-frontend/src/lib/notify.ts, popspot-frontend/src/features/popup/AddPlaceModal.tsx


    관련 글

  • 이전 — v2.5, 게스트 재배선
  • 다음 — v2.7, 주소창 권한 위조 / 티켓 도용 (IDOR) / X-Forwarded-Host 헤더 위조
  • 공유

    댓글