본문 바로가기
FrontEnd Develop/Project : AI TUTOR

튜토리얼 모달 컴포넌트 기능 개발 #1. 기본 설계서

by Frisbeen 2025. 3. 9.

 

첫 task인 튜토리얼 (온보딩) 모달을 개발하며 했던 고민들을 저장했다

TypeScript와 Next.js 환경에서의 최적화된 방식은 무엇일까 고민하다가...

 

 

 요구사항 : 모달을 만들때 요청 받았던건 소개 이미지 + 캐러셀 조합으로 튜토리얼 가이드가 필요하다.

 

1. 두개의 커다란 덩아리

2. 일단 전체적으로 두개의 컴포넌트로 나누면 좋겠다 싶었다

 

 


1. 컴포넌트 분리의 필요성  (유지보수와 성능개선)

단일 책임 원칙 (SRP: Single Responsibility Principle)

컴포넌트가 하나의 역할만 수행하도록 설계해야 유지보수가 쉬워지겠다.

 가독성과 재사용성 증가

모든 기능을 한 파일에서 처리하면 코드가 복잡해지고 유지보수가 어려워지겠다.

 성능 최적화 (Next.js 환경 고려)

  • 서버 사이드 렌더링(SSR) 방지 
  • 불필요한 렌더링 최소화 (메모리 절약 및 UI 성능 개선) 

2. 컴포넌트 분리 구조?

아래처럼 설계를 일단 했다

📂 components
 ├── 📂 modals
 │   ├── OnBoardingModal.tsx  ✅ (온보딩 모달 전체)
 ├── 📂 carousel
 │   ├── OnBoardingCarousel.tsx  ✅ (캐러셀 UI 분리)

각 컴포넌트의 역할은 명확하다. (단일 책임 원칙을 준수)

  • OnBoardingModal.tsx → 모달 전체를 담당 (배경 및 레이아웃 포함)
  • OnBoardingCarousel.tsx → 캐러셀(슬라이드 UI) 담당

3. OnBoardingModal.tsx 기본 설정

React.FC<T> 타입은 개인적으로 그냥 이 모달의 props type (interface)을 최우선으로 지정한다.

 

근거는? (왜 props interface를 지정해야하는가)

-> 이 모달은 어디서인가 쓰이는 컴포넌트이다. (이번 프로젝트 같은 경우 HomePage에서 모달이 띄워진다)

그럼 당연히 이 Modal.tsx는 하나의 HomePage.tsx의 자식컴포넌트로써 Props로 제어를 받아야한다.

 

더 구체적인 예시가

예를 들면, 홈페이지에서 모달이 띄워지면 누군가는 닫아야한다. 이걸 모달 자체에서 닫을래 vs 홈페이지에서 닫을래

당연히 후자를 선택한다.

 

홈페이지에서 모달을 클릭했을때 닫히던가 말던가 해야하니까 제어권은 홈페이지에 있다

-> 그렇기에 더더욱 props 설정을 해야한다는 것이다.

 

어떻게 제어함?

1. 핸들러로 할수도있겠고 (Page.tsx) <Route:Home> -> 직접 제어

2. 아니면 닫는 함수 (Hook)를 따로 밖으로 빼서 import 해준 후  제어 -> 간접 제어 

 

방식은 다양하겠다.

 

 

OnBoardingModalProps Interface  <T>에 넣을것

interface OnBoardingModalProps {
  onClose: () => void;
}
  • 부모 컴포넌트에서 onClose를 props로 전달하여 모달을 닫을 수 있도록 해야 함
  • 모달 바깥을 클릭했을 때도 닫히도록 설정.

 

이벤트 버블링을 고려한 모달 닫기 (stopPropagation

 

이벤트 버블링은

기본적으로 자식 요소에서 발생한 이벤트가 부모 요소로 전파되는 것이다.

 자식 요소에서 클릭 이벤트가 발생하면, 해당 이벤트가 부모 요소에도 전달되어 부모 요소에 등록된 onClick 핸들러도 실행될 수 있다.

 

따라서 하위에서 누른 이벤트로 인해 의도치 않게 부모의 이벤트가 발생할수 있기에 이를 방지하려고 자식 이벤트 내부에서 부모 이벤트와 같은 이벤트가 발생되었을때, e.stopPropagation()을 실행시켜 의도치 않은 전파 현상을 방지하는 것이다.

 

모달의 닫히는 기능을 구현한다고 가정했을때 난 모달의 밖을 클릭했을때 모달이 꺼지게 하고 싶다.

그래서 최외각 DOM에 onClick = {onCLose}를 했다

근데 만약 모르고 내부를 클릭했다면?

내부 클릭했더니 이벤트 버블링으로 인해 최외각까지 전해져서 의도치 않은 onClick ={onClose}가 실행된다.

 

따라서 그걸 방지하려면.. 자식 DOM요소에서 막으면 되겄다.

 

import React from 'react';

interface ModalProps {
  onClose: () => void;
}

const Modal: React.FC<ModalProps> = ({ onClose, children }) => {
  return (
    // 최외각 DOM: 이 영역을 클릭하면 onClose가 호출되어 모달이 닫힙니다.
    <div
      onClick={onClose}
      className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
    >
      {/* 내부 DOM: 이 영역을 클릭하면 이벤트 버블링을 막아 onClose가 호출되지 않게 합니다. */}
      <div
        onClick={(e) => e.stopPropagation()}
        className="bg-white p-6 rounded-md"
      >
        {children}
      </div>
    </div>
  );
};

export default Modal;

 

<div onClick={onClose}> // ✅ 배경 클릭하면 닫기
  <div onClick={(e) => e.stopPropagation()}> // ✅ 내부 클릭 시 닫히지 않도록 설정

 

더 쉽게 표현하면 위 코드겠다. 이제 이해 되쥬

 

모달의 기본 설정은 여기까지

 


 4. OnBoardingCarousel.tsx  기본 설정

 

일단 캐러셀은 라이브러리의 것을 차용할것이다. 

그러나 Next.js 환경이란걸 잊으면 안됌

Next.js에서는 react-slick을 동적으로 로드해야 함 (dynamic import)

import dynamic from "next/dynamic";
const Slider = dynamic(() => import("react-slick"), { ssr: false });
  • Next.js에서는 SSR(서버 사이드 렌더링) 시 window 객체가 없기 때문에 클라이언트에서만 실행되도록 설정해야 함.
  • window가 쓰일만한건 일단 서버 쪽에선 차단 시켜줘야한다

캐러셀 설정 값 (settings) 정의

const settings = {
  dots: true,
  infinite: false,
  speed: 500,
  slidesToShow: 1,
  slidesToScroll: 1,
  arrows: true,
};
  • 한 번에 몇 개의 슬라이드를 표시할지 (slidesToShow)
  • 페이지 네이션 (dots)
  • 좌우 이동 버튼 (arrows)

디자이너님의 디자인에 따름. 디자이너가 있으면 넘 좋은것 같다. 정말로


  5. useOnboarding  커스텀 훅 , localStorage 저장 헬핑 함수 <관심사 분리>

온보딩 모달 상태를 관리하는 로직 (로컬 스토리지에 있냐 없냐)을 직접 useOnboarding( showing role)에서 관리하면 되잖아요

왜 분리함?

 

-> 컴포넌트 분리 하듯, 사실 함수 자체도 분리하면 좋을 것 같았다. (물론 동료 프론트엔드 개발자 유진이가 알려준 아이디어긴함)

-> 한 함수가 다 때려박아도 가능하지만, 말그대로 관심사 분리하는 방향이 옳아 보인다.

-> 컴포넌트 분리하듯 했다

 localStorage를 활용한 상태 저장 (헬핑 함수)

사용자가 처음 홈페이지에 접속하고 튜토리얼 모달을 보고 모달을 끄면 해당 상태가 저장된다.

여기서도 typeof window 이거 왜할까? ssr 안전장치 겠다.

export const getIsFirstTimeUser = (): boolean => {
  if (typeof window !== "undefined") {
    const value = localStorage.getItem("isFirstTimeUser");
    return value === null || value === "true";
  }
  return true;
};

localStorage를 이용해 사용자가 온보딩을 이미 봤는지 체크한다.

이런 기능을 우리가 실제로 HomePage에서 쓰일 곳에 커스텀훅으로 전달한다.

 

 커스텀 훅을 사용하여 상태 관리 (useOnboarding.ts)

import { useState, useEffect } from "react";
import { getIsFirstTimeUser, setIsFirstTimeUser } from "@/utils/localStorage";

export const useOnboarding = () => {
  const [showOnboarding, setShowOnboarding] = useState(false);

  useEffect(() => {
    if (getIsFirstTimeUser()) {
      setShowOnboarding(true);
    }
  }, []);

  const closeOnboarding = () => {
    setIsFirstTimeUser(false);
    setShowOnboarding(false);
  };

  return { showOnboarding, closeOnboarding };
};

 

기능 요약

useEffect에서 localStorage를 확인하고 처음 방문한 사용자만 모달을 띄움

closeOnboarding()을 호출하면 다시 모달이 뜨지 않도록 설정


최종 정리

 컴포넌트 분리 OnBoardingModal.tsx, OnBoardingCarousel.tsx를 분리하여 유지보수성을 높임
 모달 닫기 기능 onClose props를 활용하여 배경 클릭, X 버튼 클릭 시 닫기 설정
 Next.js 환경 고려 next/image 경로 주의, react-slick dynamic import 사용
 useOnboarding 커스텀 훅 + L.S Assist 함수 적용 localStorage를 활용하여 첫 방문 시에만 모달 띄우기 구현