즐겁게 개발을...

개발보다 게임이 더 많이 올라오는 것 같은...

개발/Node.js

Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #3

다물칸 2023. 4. 5. 16:28
728x90

업데이트일자: 2023.04.05

개발스택 및 종속버전

더보기

참고했던 글들이 100% 완벽하게 되지 않아서 3~4일 헤메다 얼추 기반 프로젝트에 준하는 결과물을 만들 수 있을 것 같아 작성해봅니다. 아마 몇 주, 몇 달 후에 이 글을 따라 하시는 분들도 저와 같이 100% 성공하지는 못할 수도 있습니다.

자바스크립트 진영은 매일/매시간 패키지들이 업데이트 되는 터라 서로 버전이 맞지 않아 발생될 수도 있는 일이니 그 부분 염두에 두고 이 글을 봐주셨으면 감사하겠습니다. 

개발 스택

Language Typescript
Backend Framework Nest.js
Frontend Framework Next.js (for React.js)
Database Sqlite
Database ORM Prisma
UI Framework Chakra-ui
Tools ESLint

글에 앞서 이 글을 쓴 기준을 종속된 패키지 버전을 참고해서 글을 이해해주시기 바랍니다. 

종속버전

"dependencies": {
    "@chakra-ui/next-js": "^2.1.1",
    "@chakra-ui/react": "^2.5.4",
    "@emotion/react": "^11.10.6",
    "@emotion/styled": "^11.10.6",
    "@nestjs/common": "^9.3.12",
    "@nestjs/core": "^9.3.12",
    "@nestjs/platform-express": "^9.3.12",
    "@nestjs/swagger": "^6.2.1",
    "@prisma/client": "^4.12.0",
    "@types/node": "18.15.11",
    "@types/react": "18.0.31",
    "@types/react-dom": "18.0.11",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0",
    "eslint": "8.37.0",
    "eslint-config-next": "13.2.4",
    "framer-motion": "^10.9.2",
    "next": "13.2.4",
    "next-auth": "^4.20.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "swagger-ui-express": "^4.6.2",
    "typescript": "5.0.2"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.3.0",
    "dotenv-cli": "^7.1.0",
    "prisma": "^4.12.0"
  }

Node.js 버전: v18.15.0

UI를 먼저 하려다가 다국어 체계를 먼저 가져가는 게 좋을 것 같아 끼어 넣습니다. (UI는 넘사벽...)

 

next-i18Next 패키지 설치

※ 경고 메시지가 뜨길래 이 이슈를 통해 아래처럼 다 설치해야 한다고 해서 다 설치했다.

npm install --save next-i18next react-i18next i18next

언어파일 생성

public/locales/ko/common.json, public/locales/en/common.json 파일 생성

요런 식

아래 블로그에서도 이야기하지만, 하나의 파일로 관리하는 것보다 페이지 별로 관리하는 것이 좋다. 예전에 하나의 파일로 관리했다가 앞에 Prefix로 페이지 이름까지 붙여야 했던 기억이.......

 

테스트를 위해 각각 파일에 넣자.

# en/common.json
{
    "Login": "Sign in",
    "Logout:": "Sign Out",
    "Signup": "Sign up"
}

# ko/common.json
{
    "Login": "로그인",
    "Logout:": "로그아웃:",
    "Signup": "가입하기"
}

 

next-i18next.config.js 파일 생성

구성할 언어, 기본 언어 및 경로 등을 설정한다.

module.exports = {
    i18n: {
      defaultLocale: 'ko',
      locales: ['ko', 'en'],
    },
    //localePath: path.resolve('./my/custom/path'), // 별도 경로일 경우 이렇게 처리
  };

next.config.js 파일 수정

/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config');

const nextConfig = {
  reactStrictMode: true,
  i18n
}

module.exports = nextConfig;

pages/_app.tsx 파일수정

appWithTranslation를 선언하고 App함수를 감싸서 내보내는 코드를 주목하자.

import * as React from 'react';
import type { AppProps } from 'next/app';
import { SessionProvider } from 'next-auth/react';
import { ChakraProvider } from '@chakra-ui/react';
import { appWithTranslation } from 'next-i18next';

import Navbar from '@/pages/components/Navbar';
import Layout from '@/pages/components/Layout';

import '@/css/tailwind.css';

function App({
  Component,
  pageProps: { session, ...pageProps },
}: AppProps) {
  return (
    <SessionProvider session={session}>
      <ChakraProvider resetCSS>
        <Navbar />
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ChakraProvider>
    </SessionProvider>
  );
}

export default appWithTranslation(App);

pages/index.tsx 수정

아래 블로그 필자는 옵션으로 처리하라던데, 필수로 해야 된다.

next.js가 Server Side Rendering(SSR)기반이기 때문에 반드시 넣어야 한다. 

import React from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import { serverSideTranslations } from "next-i18next/serverSideTranslations";

import { Flex, Heading, VStack } from '@chakra-ui/react';

const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Based Next.js & Nest.js</title>
        <meta name="description" content="Thirdparty: Prisma & Next-auth & Chakra-UI"/>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Flex justify="center">
        <VStack mb={6}>
          <div className="py-32 text-center">
            <div className="text-4xl font-extrabold">
              <Heading mb={6}>Based Next.js & Nest.js</Heading>
            </div>
          </div>
        </VStack>
      </Flex>
    </div>
  );
};

export async function getStaticProps({ locale }: any) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ["common"])),
    },
  };
}

export default Home;

Navbar에 적용

import React, { useState } from 'react';
import { DarkModeSwitch } from '@/pages/components/DarkModeSwitch';
import { MenuIcon, XIcon } from '@heroicons/react/outline';
import { signIn, signOut, useSession } from 'next-auth/react';

import { useTranslation } from 'next-i18next';

const Navbar = () => {
  const [menuToggle, setMenuToggle] = useState(false);
  const { data: session, status } = useSession();
  const { t } = useTranslation('common');

  console.log('Session User Name: ' + JSON.stringify(session));
  console.log('Session User Status: ' + JSON.stringify(status));

  return (
    //   navbar goes here
    <nav className="bg-gray-100">
      <div className="max-w-6xl mx-auto px-4">
        <div className="flex justify-between">
          <div className="flex space-x-4">
            {/* logo */}
            <div>
              <a href="/" className="flex items-center py-5 px-2 text-gray-700">
                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
                  <path
                    fillRule="evenodd"
                    d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1zM5.618 4.504a1 1 0 01-.372 1.364L5.016 6l.23.132a1 1 0 11-.992 1.736L4 7.723V8a1 1 0 01-2 0V6a.996.996 0 01.52-.878l1.734-.99a1 1 0 011.364.372zm8.764 0a1 1 0 011.364-.372l1.733.99A1.002 1.002 0 0118 6v2a1 1 0 11-2 0v-.277l-.254.145a1 1 0 11-.992-1.736l.23-.132-.23-.132a1 1 0 01-.372-1.364zm-7 4a1 1 0 011.364-.372L10 8.848l1.254-.716a1 1 0 11.992 1.736L11 10.58V12a1 1 0 11-2 0v-1.42l-1.246-.712a1 1 0 01-.372-1.364zM3 11a1 1 0 011 1v1.42l1.246.712a1 1 0 11-.992 1.736l-1.75-1A1 1 0 012 14v-2a1 1 0 011-1zm14 0a1 1 0 011 1v2a1 1 0 01-.504.868l-1.75 1a1 1 0 11-.992-1.736L16 13.42V12a1 1 0 011-1zm-9.618 5.504a1 1 0 011.364-.372l.254.145V16a1 1 0 112 0v.277l.254-.145a1 1 0 11.992 1.736l-1.735.992a.995.995 0 01-1.022 0l-1.735-.992a1 1 0 01-.372-1.364z"
                    clipRule="evenodd"
                  />
                </svg>
                <span className="font-bold">Enjoydev.NET</span>
              </a>
            </div>
            {/* primary nav  */}
            <div className="hidden md:flex items-center space-x-1">
              <a href="#" className="py-5 px-3 text-gray-700 hover:text-gray-900">
                메뉴1
              </a>
              <a href="#" className="py-5 px-3 text-gray-700 hover:text-gray-900">
                메뉴2
              </a>
            </div>
          </div>
          {/* secondary nav */}
          {status === 'authenticated' ? (
            <div className="hidden md:flex items-center space-x-1">
              <div className="py-5 px-3 text-gray-700 hover:text-gray-900">{session.user?.name}</div>
              <button className="py-5 px-3" onClick={() => signOut()}>
                {t('Logout')}
              </button>
            </div>
          ) : (
            <div className="hidden md:flex items-center space-x-1">
              <a href="/api/auth/signin" className="py-5 px-3">
                {t('Login')}
              </a>
              <a href="/signup" className="py-2 px-3 bg-yellow-400 hover:bg-yellow-300 text-yellow-900 hover:text-yellow-800 rounded transition duration-300">
                {t('Signup')}
              </a>
            </div>
          )}
          {/* mobile menu */}
          <div className="md:hidden flex items-center">
            <button onClick={() => setMenuToggle(!menuToggle)}>{menuToggle ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}</button>
          </div>
        </div>
      </div>

      {/* mobile menu items */}
      <div className={`${!menuToggle ? 'hidden' : ''} md:hidden`}>
        {status === 'authenticated' ? (
          <div>
            <a href="#" className="block py-2 px-4 text-sm hover:bg-gray-200">
              Menu1
            </a>
            <a href="#" className="block py-2 px-4 text-sm hover:bg-gray-200">
              Menu2
            </a>
            <button className="block py-2 px-4 text-sm hover:bg-gray-200" onClick={() => signOut()}>
            {t('Logout')}
            </button>
          </div>
        ) : (
          <div>
            <a href="/api/auth/signin" className="block py-2 px-4 text-sm hover:bg-gray-200">
              {t('Login')}
            </a>
            <a href="/signup" className="block py-2 px-4 text-sm hover:bg-gray-200">
            {t('Signup')}
            </a>
          </div>
        )}
      </div>
    </nav>
  );
};

export default Navbar;

결과

URL을 잘 살펴보자. 

Localhost가 아닌 이유는 필자는 Linux를 VM에 설치하여 개발 중입니다. 가끔 localhost로 될 때도 있는데 안될 때가 있어서 ... (왜 되는지도 이해가 안되고 왜 가끔 안되는 것도 이해가 안되네요.)

한글 페이지
영문 페이지

숙제

  • Navbar에 언어선택기능을 추가하자. 

참고

 

Next.js 로 만든 프로젝트에 다국어 설정 끼얹기

 

(React) Next.js 로 만든 프로젝트에 다국어 설정 끼얹기

앗! Next.js 다국어 설정 타이어보다 쉽다!

velog.io

 

반응형