반응형

배경

오랜만에 쭈꾸미 집을 들려 안부 인사를 묻고 웹사이트는 문제 없냐 물어봤다. 그런데 직원 핸드폰에서는 접속이 잘되는데 가끔 외국인 핸드폰에서는 404 에러가 뜬다고 한다. 아니 이런.. 그게 무슨 말이야?! 왜지 왤까.. 집에가면서 계속 생각을 해봤고 아무래도 vercel 에서 해외 IP 차단을 한 것 일 수도 있다고 생각이 들었다.

 

부랴부랴 집에 와서 vercel 에서의 내 프로젝트를 확인해 오류 로그를 보려고 하니 아뿔싸 분석기는 따로 import 해서 layout.tsx 에 넣어야 작동을 한다는 것!?

 

바로 설치해.

 

Step 1 : npm 설치

 

npm i @vercel/analytics

위 명령어를 실행하면 자동으로 패키지가 설치된다.

 

 

무이스~! 아주 잘 설치가 됐군요

 

Step 2 : Layout.tsx 에 컴포넌트를 넣는다.

 

 

Layout.tsx 에 위처럼 Analytics 를 불러온다.

 

 

이후에 body 태그 안에 예쁘게 붙여 넣는다.

 

Step 3 : Publish

 

git에 저장하면 자동으로 deploy!

 

https://vercel.com/docs/analytics/quickstart#add-the-analytics-component-to-your-app

 

Vercel Web Analytics Quickstart

Vercel Web Analytics provides you detailed insights into your website's visitors. This quickstart guide will help you get started with using Analytics on Vercel.

vercel.com

 

 

 

위 사진처럼 이제 방문객을 확인할 수 있다.

그런데..

이제 해외 IP 에서 웹 사이트를 접속해보려고 VPN 을 이용해 접속해보았다. 그런데 오류가 시뮬레이션이 되지 않았다...

 

도대체 무엇이 문제일까..

지속적인 로그 확인을 통해 지켜보아야겠다.

반응형
반응형

배경


사실 다국어 언어 메뉴를 만들면서 초기구상은 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

 

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

결과물


쏘 굿~

 

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

반응형
반응형

열심히 개발을 하고 테스트를 하면서 느꼈던 점은 이미지 렌더링이 은근느리다는 것이 었다. 기능개발을 빠르게 하다보니 놓치고 있었는데 분석을 해보려고 한다.

분석

웹사이트를 분석하는 도구는 여러가지가 있는데 대표적인 하나는 구글에서 제공하고 크롬 플러그인으로 설치가능한 'Lightroom'이다. 라이트룸으로 분석을 진행한 결과 아주 놀라운 결과를 얻게 되었다.

 

 

오마이갓 LCP 는 왜저렇고 성능은 52초라니.. 진짜 미친 성능이다. 주된 원인은 다음과 같았다.

 

 

음.. 좀 많은데..? 아니 많이 많은데??

 

우선적으로 이미지 크기를 적절하게 설정하기 위해서는 내가 사용한 이미지 컴포넌트에 대해 조사를 해볼 필요성이 있다고 깨달았다.

Next/Image

이미지를 출력하기 위해 Next 에서 제공하는 Next/Image 컴포넌트를 사용한다. jpg나 png 를 webp로 자동 변환시켜 렌더링하기에 파일 크기를 크게 줄여 효율적이다. 내가 가장 많이 컴포넌트를 사용한 코드를 보면 다음과 같다.

 

<div className="w-full h-1/2 image-box">
    <Image
        src={imageUrl}
        alt={name}
        className="object-cover w-full h-full"
    />
</div>

 

이미지에 사이즈가 명확하게 설정이 되어있지 않아 이미지 원본파일의 raw data가 그대로 적용되어 이미지 로딩이 커진것이었다..! 그래서 이미지에 명확한 사이즈를 선언하기 위해 고민을 해보았다.

 

우선은 Image 에 width , height 요소를 지정해주면 되지만 나는 이미지가 부모의 width, height 에 맞게 조절되어야 하기 때문에 이미지 크기를 지정하는건 올바르지 않다.

 

어떻게하면 부모크기만큼만 이미지가 가득 채워질지..?

fill ={true}

Next/Image 에는 여러가지 속성이 있고 속성 중 하나인 fill 이 있다. fill 을 사용하게 된다면 자동적으로 postion 이 "absolute" 가 지정된다. 따라서, 부모가 fixed 혹은 relative, absolute 여야 자식의 width, height 값이 조정된다고한다.

 

따라서, 부모의 relative 를 부여하고 Image 에 fill 속성을 부여했다.

 

<div className="w-full h-1/2 image-box relative">
    <Image
        src={imageUrl}
        alt={name}
        fill
    />
</div>

 

다시 검사를 해보면 ..!

 

 

확연히 줄어든 것을 볼 수 있다!

sharp vs Squoosh

Next/Image 는 Squoosh 를 기본적으로 이미지 최적화 모듈을 사용하고 있지만 sharp 라이브러리를 사용할 것을 권장하고 있다.

 

왜냐하면? webP 기준 응답속도가 3~4배 빠르기 때문이다. 그러면 처음부터 sharp 를 쓰는게 더 효율적인거 아닌가..? 왜 굳이 한 번 더 설치를 하는 것인지 궁금하군,,

 

squoosh 사용시 응답속도

 

 

sharp 사용시 응답속도

 

 

설명대로 sharp 사용시 응답속도가 개선되는 것을 볼 수 있다.

반응형

+ Recent posts