업데이트일자: 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 로 만든 프로젝트에 다국어 설정 끼얹기
'개발 > Node.js' 카테고리의 다른 글
try~catch문에서 error타입에 따라 처리하는 함수 (0) | 2024.04.23 |
---|---|
Typescript 현재 접속한 사용자의 아이피 가져오기 (0) | 2023.08.19 |
Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #2 (0) | 2023.04.05 |
Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #1 (0) | 2023.03.30 |
[2021.03] Winston패키지를 이용한 Logger 클래스 (0) | 2021.03.26 |