반응형

배경


사실 다국어 언어 메뉴를 만들면서 초기구상은 nexti18n 을 사용하기로 했지만, 배움의 부담에 못이겨 하드코딩으로 진행했다. 그러다보니 유지보수는 물론,, 가독성이 완전 제로제로제로였다. 그래서 이를 잘 관리하기 위해 기존 기획했던 nexti18n 을 제대로 적용해보려고 한다.

 

처음부터 잘못됐던


나는 초반에 nexti18n 라이브러리를 적용하기 위해 설치를 했으나 구글링을 통해 제대로 확인해보니 Next13/14 App 기반 환경은 기존 라이브러리를 지원하지 못한다고 한다!?!

 

 

대충 app 에서는 지원하지 않는다는 의미

 

대신 i18next, react-i18next, i18next-resources-to-backend 라이브러리를 사용해 구성하라고 한다.

npm install i18next react-i18next i18next-resources-to-backend

https://locize.com/blog/next-app-dir-i18n/

 

i18n with Next.js 13/14 and app directory / App Router (an i18next guide)

Looking for a way to internationalize your Next.js 13/14 project with the new app directory / App Router paradigm? Then this guide is for you!

locize.com

 

해당 블로그를 따라하라고 하길래 보면서 열심히 따라쳤다. 하지만,, 2년전 자료를 그 때 깨닫고 하지 말았어야 했는데..

 

결론은 언어를 동적 링크를 생성해 주소를 호출하는 과정속에서 알 수 없는 404 에러가 계속 나와 다시 next i18n 에 대해 검색을 진행했다.

돌아도 한참 돌아갔던


조금더 검색을 해보니 next-intl 이라는 라이브러리가 존재한다는것을 알게되었다..! 아뿔싸 나의 내다버린 3시간.. 하지만 개발이란 원래 이런것..

 

설치과정을 잠깐 보자면

설치과정

0. 상황

├── messages (1)
│   ├── en.json
│   └── ...
├── next.config.mjs (2)
└── src
    ├── i18n.ts (3)
    ├── middleware.ts (4)
    └── app
        └── [locale]
            ├── layout.tsx (5)
            └── page.tsx (6)

샘플 폴더 구조는 다음과 같다.

 

1. 라이브러리를 설치한다.

npm install next-intl

 

2. 파일 셋업하기

1) messages/en.json

{ "Index": { "title": "Hello world!" }}
  • 각 언어마다 json 파일 형식으로 글에 들어갈 내용을 작성한다.

2) next.config.mjs

import createNextIntlPlugin from 'next-intl/plugin'; 
const withNextIntl = createNextIntlPlugin(); 
/** @type {import('next').NextConfig} */
const nextConfig = {}; 
export default withNextIntl(nextConfig);
  • next-intl 을 사용하기위한 플러그인을 설정하는 코드다.

3) i18n.ts

import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';

// Can be imported from a shared config
const locales = ['en', 'ko'];

export default getRequestConfig(async ({locale}) => {
  // Validate that the incoming `locale` parameter is valid
  if (!locales.includes(locale as any)) notFound();

  return {
    messages: (await import(`../messages/${locale}.json`)).default
  };
});
  • next-intl은 요청 범위의 구성 객체를 생성하여 사용자의 언어에 따라 메시지 및 기타 옵션을 제공할 수 있으며, 이를 서버 컴포넌트에서 사용할 수 있습니다.

4) middleware.ts

import createMiddleware from 'next-intl/middleware';

export default createMiddleware({
  // A list of all locales that are supported
  locales: ['en', 'ko'],

  // Used when no locale matches
  defaultLocale: 'en'
});

export const config = {
  // Match only internationalized pathnames
  matcher: ['/', '/(ko|en)/:path*']
};
  • middleware 에서는 요청에 따른 언어가 매칭되는지 확인하고 리다이렉트하거나 리라이트 하는 역할을 한다.

5) app/[locale]/layout.tsx

import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';

export default async function LocaleLayout({
  children,
  params: {locale}
}: {
  children: React.ReactNode;
  params: {locale: string};
}) {
  // Providing all messages to the client
  // side is the easiest way to get started
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

 

6) app/[locale]/page.tsx

import {useTranslations} from 'next-intl';

export default function Index() {
  const t = useTranslations('Index');
  return <h1>{t('title')}</h1>;
}
  • [locale] 이라는 파일 명을 통해 미들웨어에서 매칭된 언어인 경우 주소값을 가질 수 있다.

결과


테스트 성공!

기존 웹사이트에 적용하기


이제 next-intl 을 적용되었으니 기존 사이트에 추가해야한다. locale (언어 변수) 를 전역으로 관리해야한다는 아이디어가 생겨 상태를 관리할 수 있는 LanguageContext.tsx 를 만들었다.

// context/LanguageContext.tsx

"use client"
import { createContext, useContext, useState, ReactNode } from 'react';
import { Language } from '../types';

interface LanguageContextProps {
  selectedLanguage: Language;
  setSelectedLanguage: (language: Language) => void;
}

const LanguageContext = createContext<LanguageContextProps | undefined>(undefined);

interface LanguageProviderProps {
  children: ReactNode;
  initialLocale: Language;
}



export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {
  const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);

  return (
    <LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};



export const useLanguage = (): LanguageContextProps => {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
};

 

로직

 

1. layout.tsx 에서 locale 을 다룰 수 있는 Provider를 만들어 감싼다.

<html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          <LanguageProvider initialLocale={locale}>
          {children}
          </LanguageProvider>
          <div id="global-modal"></div>
          <div id="howtoeat-modal"></div>
        </NextIntlClientProvider>
      </body>
    </html>

 

이렇게 감싸게 되면 LanguageProvider 에 저장된 selectedLanguage 변수로 locale 을 children 에 전달하는 방법을 통해 언어 변수를 관리할 수 있다.

 

2. LanguageProvider selectedLanguage

export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children, initialLocale }) => {

  const [selectedLanguage, setSelectedLanguage] = useState<Language>(initialLocale);

  return (
    <LanguageContext.Provider value={{ selectedLanguage, setSelectedLanguage }}>
      {children}
    </LanguageContext.Provider>
  );
};

 

여기서 저장된 selectedLanguage 는 다음 함수를 호출함으로 외부에서 사용할 수 있다.

 

3. useLanguage

export const useLanguage = (): LanguageContextProps => {
  const context = useContext(LanguageContext);
  if (context === undefined) {
    throw new Error('useLanguage must be used within a LanguageProvider');
  }
  return context;
};
const {selectedLanguage, setSelectedLanguage} = useLanguage();
const router = useRouter();
const pathname = usePathname();

  useEffect(() => {
    const currentLocale = pathname.split('/')[1] as Language;
    if (currentLocale && ["ko", "en", "ja", "th", "ch"].includes(currentLocale)) {
      setSelectedLanguage(currentLocale);
    } else {
      console.error(`Invalid language in path: ${currentLocale}`);
    }

  }, [pathname, setSelectedLanguage]);

 

위와 같이 page.tsx 에서 selectedLanguage 변수에 useLanguage() 로 context 를 전달한다. 이렇게 만든다면 드롭다운을 통해 언어를 선택하고 , 선택된 언어는 Context 로 관리를 할 수 있다.

다음 할 일


이제 복잡한 건 끝났고, 언어별 json 파일을 만들어서 좀 더 보기좋은 코드로 만드는 일만 남았다. 상당히 귀찮고 복잡한 일일 것으로 예상되지만 이것 역시 내가 극복 해야할일...

 

또 생각이 든건데 LanguageContext가 아닌 redux 를 사용해도 뭔가 될 것 같은 기분이다. redux에 대해 제대로 배우질 않아서 적용하지 않았는데 나중에 적용해봐도 될 것같다.

반응형
반응형

배경


메뉴의 상세 정보 모달을 띄우게 되면 대표 이미지가 나오게 된다. 이때, 다양한 사진을 제공하는게 좋을 것 같아 Carousel 을 적용해 보려고 한다.

 

어떤걸 쓸까?


  1. 직접구현
  2. Swiper

우선은 문서화가 잘되어 있고 배우기 편하고 설치가 간단한 Swiper 를 대중적으로 많이 사용하는 것 같은 분위기처럼 보인다. 마음 같아서는 직접 구현하면서 공부하고 싶지만 나에게 그런 시간은 존재하지 않는다... 얼른 이 프로젝트를 끝내고 싸피에 집중해야할 것 같다.

 

또한, 좀 찾아보니 구글링을 좀만 하더라도 대부분 Swiper 얘기를 하는거보니 대세인것 같다. 나는 대세를 따르겠다.

 

Swiper


https://swiperjs.com/get-started

 

Swiper - The Most Modern Mobile Touch Slider

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

 

공식문서

 

$ npm install swiper

 

우선 swiper 를 설치한다.

// import Swiper JS
import Swiper from 'swiper';
// import Swiper styles
import 'swiper/css';

const swiper = new Swiper(...);

 

swiper 는 위와 같이 선언해 사용가능하다. 다시 해보면 알겠지만 위 코드로는 뭔가가 애매하다는 것을 알 수 있다.

 

하지만, 공식문서에 나온 방법으로는 먼가 잘 안되서 Swiper 제작팀이 만든 데모 페이지를 통해 솔루션을 얻을 수 있게 되었다.

How to?


import React, { useRef, useState } from "react";

// Import Swiper React components

import { Swiper, SwiperSlide } from "swiper/react";

// Import Swiper styles

import "swiper/css";

import "./styles.css";

export default function App() {
  return (
    <>
      <Swiper className="mySwiper">
        <SwiperSlide>Slide 1</SwiperSlide>
        <SwiperSlide>Slide 2</SwiperSlide>
        <SwiperSlide>Slide 3</SwiperSlide>
        <SwiperSlide>Slide 4</SwiperSlide>
        <SwiperSlide>Slide 5</SwiperSlide>
        <SwiperSlide>Slide 6</SwiperSlide>
        <SwiperSlide>Slide 7</SwiperSlide>
        <SwiperSlide>Slide 8</SwiperSlide>
        <SwiperSlide>Slide 9</SwiperSlide>
      </Swiper>
    </>
  );
}

 

8개의 슬라이드를 출력하는 데모 코드다. Swiper 컴포넌트의 mySwiper 는 Swiper 에서 제공하는 기본 css 파일에 있는 클래스 인 것 같다. ./styles.css 에는 존재하지 않아 그렇게 보인다.

 

이 코드만 적용하게 된다면 잘 움직이는 캐러셀이 만들어진다! 두둥

 

하지만, 나는 밑에 있잖아 그거 인덱스마다 동그라미 움직이는거 먼지알지?? 그거 하고 싶어요@ 찾아보니까 Paging 이라는 속성이 있었다! 적용하기 위해 위의 코드를 살짝쿵 수정해보았다.

import "swiper/css";
import 'swiper/css/pagination';
import { Swiper, SwiperSlide } from "swiper/react";
import { Pagination} from 'swiper/modules';

    <Swiper pagination={true} modules={[Pagination]} className="mySwiper">
        <SwiperSlide>Slide 1</SwiperSlide>
        <SwiperSlide>Slide 2</SwiperSlide>
        <SwiperSlide>Slide 3</SwiperSlide>
        <SwiperSlide>Slide 4</SwiperSlide>
        <SwiperSlide>Slide 5</SwiperSlide>
        <SwiperSlide>Slide 6</SwiperSlide>
        <SwiperSlide>Slide 7</SwiperSlide>
        <SwiperSlide>Slide 8</SwiperSlide>
        <SwiperSlide>Slide 9</SwiperSlide>
      </Swiper>

 

이렇게 하면 예쁜 캐러셀이 만들어집니다~

 

https://swiperjs.com/demos#navigation

 

Swiper Demos

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

 

데모는 여기서 확인하면 이해하기 쉽게 리액트, 뷰 등 코드를 제공하니 꼭 둘러보라

결과물


쏘 굿~

 

아직은 이미지가 더 없어 추가하지 못했고, 캐러셀에 이미지를 리스트화해서 넣어야 하는데 메인 화면 코드가 상당히 지저분해서 조금 고민을 한 후 수정을 해야할 것 같다.. 이런..

반응형
반응형

Use Case

  1. 메뉴 출력을 N x 2 로 만들고 메뉴 마다 디테일 모달 띄우기
  2. Sticky Navbar 제작
  3. How to Eat

디테일 모달

현재 메뉴판은 한 줄로 쭉 나열되어있고 메뉴 카드마다 메뉴명, 메뉴 한국어명, 메뉴 디테일, 가격 이렇게 정보가 한꺼번에 나와있다. 내 생각에는 정보가 한 번에 다 나오면 귀찮은 동작없이 메뉴를 살필 수 있다고 생각하는데 아무래도 메뉴가 하나씩 나오게 되면 사용자 입장에서는 더 많은 스크롤을 내려야 한다. 따라서, N * 2 로 메뉴 리스트를 바꾸고 각 메뉴를 눌렀을 때 디테일 모달이 나오도록 변경하려고 한다.

우선, 모달 띄우기 전 화면에 출력할 내용은 외국어 메뉴명, 한국어 메뉴명, 가격이다. 이후 각 메뉴의 Element 에 onClick 을 부여해 데이터를 모달에 디테일 한 작업을 추가할 예정이다. 추후에 디테일 모달에는 Carousel 을 넣고 싶다.

 

N x 2의 메뉴

 

.menu-list {
    @apply flex flex-wrap gap-4 justify-center items-center;
}

@media (max-width: 600px) {
    .menu-list {
        @apply flex-row;
    }
}

 

gap 클래스를 기호에 맞게 수정해 주어 카드간의 간격을 조정하고, .menu-list 클래스에 flex-row로 변경하여 카드가 열별로 출력되게 변경했다.

 

이제 디테일 모달을 만들차례

 

만들려고 시도를 하자마자 클릭 이벤트를 넣는데 문제가 발생했다.

어이없는 오류 1 : 부모 Div 안에 모달 넣지말자

문제 상황

처음에는 모달을 열고 닫는 간단한 기능을 구현하려고 했다. MenuItemCard 컴포넌트에서 카드 요소를 클릭하면 모달이 열리고, 모달 내부의 닫기 버튼을 클릭하면 모달이 닫히도록 했다. 코드의 핵심 부분은 다음과 같다.

<div className="rounded-lg shadow-md bg-white overflow-hidden w-72 h-150 card" onClick={handleModalOpen}> 
    <Image src={imageUrl} alt={name} className="object-cover w-full h-1/2" width={240} height={100} /> 
    <div className="p-4 h-1/2"> 
        <h3 className="text-xl font-extrabold mb-2">{name}</h3> 
        <div className="text-md font-normal">{discription}</div> 
        <div className="text-gray-700 card-price">{price}</div> 
    </div> 
    {isModalOpen && ( 
        <DetailModal open={isModalOpen} onClose={handleModalClose}> 
            <div className="flex h-11/12 bg-blue-700"> {/* 모달 내용 */} </div> </DetailModal> )} 
</div>

 

이렇게 보면 당연히! 동작되었을 줄 알았지만 모달창이 영원히 닫히지 않는 문제가 생기게 되었다. 도대체 무엇이 문제였을까?!

 

문제 원인

문제의 원인은 모달이 부모 divonClick 이벤트에 의해 닫히는 것이었다. 모달을 클릭할 때도 부모 divonClick 이벤트가 호출되면서 setIsModalOpen 함수가 번복되는 상황이 발생했다.

 

해결 방법

해결 방법은 간단했다. onClick 이벤트를 부모 div에서 제거하고, 필요한 자식 요소에만 이벤트 핸들러를 설정하면 된다.

 

이 따위 문제를 2시간 넘게 잡고 있다는 것. 개발자로 살아남아가고 있다는 것.

 

수정된 코드
<div className="rounded-lg shadow-md bg-white overflow-hidden w-72 h-150 card">
            <div onClick={handleModalOpen}>
                <Image
                    src={imageUrl}
                    alt={name}
                    className="object-cover w-full h-1/2"
                    width={240}
                    height={100}
                />
                <div className="p-4 h-1/2"  >
                    <h3 className="text-xl font-extrabold mb-2">{name}</h3>
                    <div className="text-md font-normal">{discription}</div>
                    <div className="text-gray-700 card-price">{price}</div>
                </div>
            </div>

            {isModalOpen && (
                <DetailModal open={isModalOpen} onClose={()=>handleModalClose()}>
                    <div className="flex h-11/12 bg-blue-700">
                        {/* 모달 내용 */}
                    </div>
                </DetailModal>
            )}
        </div>

 

결과물

 

 

한국어 메뉴일 때는 공백이 매우 신경쓰이는데 일단 디자인 적인 부분은 나중에 신경쓰기로 했다. 깨알 수정안한 맛있게먹는법의 흔적

 

다음에는 How To Eat 모달을 수정할 예정!

반응형
반응형

배경


필자는 열정도 쭈꾸미 음식점에서 일하고 있다. 일을 하면서 외국인 손님이 하루에 한 팀은 꼭 오는 것을 알게 되었다. 일본인 비중이 많아 일본인 전용 메뉴판을 하나 만들어 두었다. 하지만, 가끔, 중국, 대만, 태국, 등 다양한 국적의 사람들이 오고 쭈꾸미라는 상상하기 어려운 음식을 사진없이 메뉴판만으로는 어렵다는 생각이 들었다. 메뉴판을 일일이 만들기 어려워 웹 사이트로 간단히 QR 코드 한방에 확인가능 하면 좋을 것 같아. 계획했다.

 

개발환경


프론트엔드 개발자를 목표로 하고 있기에 왠만하면 서버없이 구동을 하려고 한다. 혹시나, 서버를 사용할 수도 있기에 요새 배우고 있는 NextJS 를 이용하고자 한다. 배포 서버는 역시 무료 업체인 Vercel에 배포를 하려고 한다.

사용 프레임워크 : NextJS
배포 환경 : Vercel

 

요구사항


우선, 요구사항을 분석해보겠다.

  1. 외국인 손님을 위한 언어변환이 가능한 메뉴판
  2. 이해하기 쉬운 메뉴와 사진 제공

당장에 구현해야할 요구사항은 다음과 같다고 생각한다.

 

1. 언어변환


처음에는 실시간 변환을 하는게 어떨까 생각이 들었는데 Next에 대한 이해가 부족해서 그런지 Next i18 을 사용하기엔 조금 개발이 걸릴 것 같아 노가다로 진행하기로 했다.

 

Dropdown Button

 

한국어, 영어, 일본어, 중국어, 태국어 총 5가지 언어를 지원하기로 했다. 일해본 결과 5개언어면 될 것 같다.

"use client";

import { useRouter, usePathname } from "next/navigation";

type Language = 'ko' | 'en' | 'ja' | 'th' | 'ch';

type Props = {
    selectedLanguage: Language;
    setselectedLanguage: (language: Language) => void;
};


const MenuDropdown = ({ selectedLanguage, setSelectedLanguage }: Props) => {
    const router = useRouter();
    const changeLanguage = (lng: Language) => {
        setSelectedLanguage(lng);
    };
    return (
        <select
            value={selectedLanguage}
            onChange={(e) => changeLanguage(e.target.value as Language)}
            className="block  px-4 py-2 text-base text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
        >
            <option value="ko">한국어</option>
            <option value="en">English</option>
            <option value="ja">日本語</option>
            <option value="ch">中國語</option>
            <option value="th">ภาษาไทย</option>
        </select>
    );
};

export default MenuDropdown;

 

이렇게 5개언어를 그다지 이쁘지 않은 드롭다운 디자인과 함께 배치해 보았다.

2. 메뉴카드


이제 사진과 함께 메뉴설명을 적을 차례. 직원 친구가 외국어 메뉴와 함께 한글도 있으면 주문받기 편할 것 같다고 말해줘 적용하기로 했다.

 

 

우선 결과물 부터

 

처음에는 언어별 메뉴판 데이터를 구성해보았다. ts 를 써서 그런지 타입 정의하는 것부터 해맸다.

const translations :MenuTranslations = {

    ko: {

        title: "3인 세트 메뉴",

        dishes: ["쭈꾸미 시그니처 세트", "쭈꾸미삼겹 시그니처 세트"],

        prices:["51,000원","55,000원"],

        imageUrl :[img,img2],

        discriptions:["철판 쭈꾸미2 + 우동사리 + 계란찜(치즈변경 +1,500) + 소금구이","철판 쭈꾸미삼겹2 + 우동사리 + 계란찜(치즈변경 +1,500) + 소금구이"],

        title2:"2인 세트 메뉴",

        dishes2: ["쭈꾸미 세트", "쭈꾸미삼겹 세트", "구이 세트"],

        prices2:["36,000원","40,000원","38,000원"],

        imageUrl2 :[img3,img4,img5],

        discriptions2:["철판 쭈꾸미2 + 우동사리 + 계란찜(치즈변경 +1,500)","철판 쭈꾸미삼겹살2 + 우동사리 + 계란찜(치즈변경 + 1,500)","택2 (소금/쭈삼(+2,000)/양념/꼼장어(+1,000) + 떡사리 + 계란찜(치즈변경 + 1,500)"],

        title33:"단품 메뉴",

        dishes33: ["철판 쭈꾸미", "철판 쭈꾸미 삼겹살", "소금구이", "쭈삼구이", "양념구이","꼼장어구이"],

        prices33:["14,000원","16,000원","15,000원","17,000원", "15,000원","16,000원"],

        imageUrl33 :[img6,img7,img8,img9,img10,img11],

        discriptions33:[],

        title3:"사이드 메뉴",

        dishes3: ["화산폭발계란찜", "화산폭발치즈계란찜", "깨불고기 주먹밥", "철판볶음밥", "볶음밥 치즈추가","바지락 백탕"],

        prices3:["5,000원","6,500원","5,000원","4,000원", "3,000원","8,000원"],

        imageUrl3 :[img12,img13,img14,img15,img16,img17],

        discriptions3:[],

        title4:"사리 메뉴",

        dishes4: ["우동 사리", "떡 사리", "치즈 사리", "삽겹 사리(200g)", "미나리 사리", "날치알"],

        prices4:["3,000원","3,000원","3,000원","6,000원","4,000원","3,000원"],

        imageUrl4 :[img18,img19,img20,img21,img22,img23],

        discriptions4:[],

        title5:"주류 / 음료",

        dishes5: ["소주(참이슬, 처음처럼, 진로, 새로)", "청하", "한라산","맥주(카스, 테라, 켈리, 크러시)","논알콜 맥주(칭따오, 하이네켄)","음료(콜라, 사이다, 제로콜라, 제로사이다)","하이볼","열정 하이볼"],

        prices5:["5,000원","6,000원","6,000원","5,000원","6,000원/8,000원","2,000원","8,000원","8,000원"],

        imageUrl5 :[img24,img25,img26,img27,img28,img29,img30,img31],

        discriptions5:[],

    },
    en:{
    //
    //
    }
    ...
    };

 

코드는 이렇게 달랑 하나지만 포맷을 자꾸 바꿔서 정말 힘들고 귀찮고 막그랬다. 이렇게 언어별로 다하니까 정말 가독성 떨어지는데 나중에 리팩토링을 하던가 해야겠다.

 

const MenuList = ({ selectedLanguage }: Props) => {

    const [menu, setMenu] = useState(translations[selectedLanguage] || defaultMenu);



    useEffect(() => {

        setMenu(translations[selectedLanguage] || defaultMenu);

    }, [selectedLanguage]);



    return (

        <div className="space-y-8">

            {[menu.title, menu.title2,menu.title33, menu.title3, menu.title4, menu.title5].map((title, index) => (

                <div key={index}>

                    <h1 className="text-3xl font-bold text-center mb-4" id={index.toString()}>{title}</h1>

                    <div className="menu-list">

                        {[menu.dishes, menu.dishes2,menu.dishes33, menu.dishes3, menu.dishes4, menu.dishes5][index].map((dish, dishIndex) => (

                            <MenuItemCard

                                key={dishIndex}

                                name={dish}

                                price={[menu.prices, menu.prices2,menu.prices33, menu.prices3, menu.prices4, menu.prices5][index][dishIndex]}

                                imageUrl={[menu.imageUrl, menu.imageUrl2, menu.imageUrl33, menu.imageUrl3, menu.imageUrl4, menu.imageUrl5][index][dishIndex]}

                                discription={[menu.discriptions,menu.discriptions2,menu.discriptions33,menu.discriptions3,menu.discriptions4,menu.discriptions5][index][dishIndex]}

                            />

                        ))}

                    </div>

                </div>

            ))}

        </div>

    );

};

 

이제 만든 데이터를 통해 map 함수를 이용해서 카드를 생성한다.

다른 언어일때의 모습은

 


요로코롬 아주 이쁜 카드가 만들어진다.

사진은 아직 직원친구가 이쁜 사진들을 안보내줘서 대체사진을 사용중이다. 이정도면 쓸만하겠지?

3. 메뉴별 이동버튼


개발을 하고나니 스크롤이 너무 길다는 불편함이 있었다. 그래서 버튼 두개를 만들어 하나는 맨위로 올라가는 버튼과 하나는 누를 때 마다 다음 메뉴 카테고리로 이동하는 버튼을 구현했다.

현재 위치 확인

useEffect를 통해 현재 위치와 가장가까운 title id를 찾는다.

    useEffect(() => {

        window.addEventListener("scroll", handleScroll);
        return () => {
            window.removeEventListener("scroll", handleScroll);
        };
    }, []);

    const handleScroll = () => {
        const offsets = titles.map(title => {
            const element = document.getElementById(title);
            // 요소가 존재하지 않는 경우, 매우 큰 값을 반환하여 화면 밖으로 간주하도록 합니다.
            return element ? window.pageYOffset + element.getBoundingClientRect().top : Number.MAX_VALUE;
        });
        const closest = offsets.reduce((prev, curr, index) => {
           if (curr !== Number.MAX_VALUE && (Math.abs(curr - window.pageYOffset) < Math.abs(offsets[prev] - window.pageYOffset))) {
                return index;
            }
            return prev;
        }, 0);
        setCurrentIndex(closest);
    };
  • titles 배열에서 각 타이틀의 ID를 사용하여 해당하는 DOM 요소를 찾는다. document.getElementById(title)는 ID가 title인 요소를 반환한다.
  • 각 요소의 위치는 element.getBoundingClientRect().top + window.pageYOffset을 통해 계산된다. 여기서 getBoundingClientRect().top은 요소의 상단 경계로부터 뷰포트 상단까지의 거리를 제공하고, window.pageYOffset은 문서가 수직으로 얼마나 스크롤 되었는지를 나타낸다. 이 두 값을 더하면 페이지 최상단으로부터의 절대적인 거리가 된다.
  • 만약 특정 타이틀의 요소가 없다면(elementnull인 경우), Number.MAX_VALUE를 반환하여, 해당 타이틀을 스크롤 위치 계산에서 제외시킨다.
  • reduce 함수는 offsets 배열을 순회하면서 각 요소와 현재 스크롤 위치와의 차이가 가장 작은 요소의 인덱스를 찾는다. 이 인덱스는 현재 뷰포트에 가장 가까운 요소를 나타낸다.
  • 계산된 closest 인덱스는 setCurrentIndex를 통해 상태로 저장되어, 다른 부분의 렌더링이나 로직에서 사용될 수 있다.

이렇게 저장된 currentIndex를 통해 다음 타이틀 이동 함수를 만들 수 있다.

const scrollToNextTitle = () => {

        if (currentIndex + 1 < titles.length) {

            const nextTitleElement = document.getElementById(titles[currentIndex + 1]);

            if (nextTitleElement) {

                nextTitleElement.scrollIntoView({ behavior: "smooth" });

                setCurrentIndex(currentIndex + 1);

            }

        }

    };

 

현재 인덱스보다 하나 앞선 인덱스의 위치를 가져오고 smooth 하게 움직이는 함수다.

맨 위로 가는 함수는 상당히 간단하다.

 

const scrollToTop = () => {

        window.scrollTo({

            top: 0,

            behavior: "smooth"

        });

    };

 

이렇게 하고 이쁜 버튼과 함께 배치해보겠다.

 

 

 

 

다음에는 사진을 좀 제대로 넣고 수정하고 싶다. 그리고, 메뉴가 오류가 났는데 그냥 메뉴 이름만 수정하면돼서 문제없을 것 같다.

 

그리고 맛있게 먹는 방법과 메뉴 눌렀을 때, 메뉴 상세정보도 띄우고싶다. 그리고 방문수 확인하는것도! 이건 백있어야하나?

반응형

+ Recent posts