동기분과 함께 새로운 사이드 프로젝트를 진행하기 위해, repository 를 구성하는 과정에서 한번 front web 과 server 를 하나의 repository 에서 관리해보고자 하였다. 다만 현재 진행하려는 서비스가 monorepo 개념이 필요한 것인지는 의문이었으며, 조금 더 monorepo의 활용방안과 의의를 알기 위해 계속 공부중이다.
이번 포스팅에서는 간단하게 Nx 를 활용하여 초기 Next.js, Tailwindcss, React-query 를 세팅한 경험을 다루고자 한다. 조금 더 monorepo에 대한 개념이 잡힌다면 그때 monorepo에 대한 자세한 포스팅을 준비해야겠다. (왜 turborepo 대신 Nx 를 선택했는지부터..)
Nx 의 workspace 를 생성하면서..
yarn workspace 나 npm workspace 처럼 기존에도 사용자들이 monorepo 환경을 꾸리는데 도움을 주는 것들이 있지만, 조금 더 높은 편의성을 제공하는 툴은 Nx와 vercel 의 turborepo 라고 생각한다. 현재 진행하려는 사이드 프로젝트에서는 딱히 두 가지 중 어느것으로 세팅해도 큰 문제가 없지만, server 단에서 nest 를 활용하기에 nx 가 좀 더 편리한 점이 있다고 해서, nx 를 선택하게 되었다.
초기 셋팅에서는 우여곡절이 많이 있었는데, 아무래도 처음 사용해보기도 하였고 개념적으로도 이해수준이 높지 않아서였다 생각한다. (그래서 이번 포스팅의 내용 역시 정답이라곤 할 수 없을 것 같고, 앞으로 프로젝트를 진행하면서 좀 더 수정해나가야 할 듯 싶다.) 그래도 고생 끝에 어느정도 세팅을 마무리 할 수 있었고, 까먹기(?) 전에 블로그에 정리를 하고자 한다.
https://nx.dev/packages/next/documents/overview
Overview
The Next.js plugin for Nx contains executors and generators for managing Next.js applications and libraries within an Nx workspace. It provides: - Scaffolding for creating, building, serving, linting, and testing Next.js applications. - Integration with bu
nx.dev
위 링크는 Nx 에서 제공하는 첫 workspace 설정 시 Next 를 기반으로 설정하는 방법이다. 실제로 workspace(프로젝트 작업이 이루어지는곳)를 처음 설정할 때, 여러 프레임워크들을 선택할 수 있다. 대표적으로 angluar, react, nest 등이 있다.
이번 프로젝트에서는 첫 세팅을 어떤 프레임워크로 지정해서 하지 않았기 때문에, next.js 를 따로 셋팅 해주어야 했다. 일단 먼저 workspace 를 생성해보자
npx nx create-nx-workspace@latest 프로젝트명
생성하고자 하는 디렉토리에 위와 같이 workspace 를 생성해주자. 그러면 아래와같이 몇가지 질문이 나오고 대답을 해주면 설치가 된다. stack 설정은 보통 프레임워크를 설정하는것이며, Package-based 냐 intergrated 냐의 질문은 단어 뜻 그대로 각 프로젝트마다 pakage 를 관리할 것인지, 아니면 하나로 통합해서 관리할 것인지에 대한 선택이다. monorepo 의 기능을 살리고자 나는 intergrated 로 하였다.
설치가 마무리되면, 이제 apps 라는 폴더와 libs, tools 라는 폴더가 생성이 되었음을 확인할 수 있다. 간단히 요약하자면 apps 라는 폴더에 실제 프로젝트가 관리가 되며, libs 는 라이브러리의 준말으로서 공용으로 사용될 여러 UI Component 나 API, library 등을 설정할 수 있다. (이 부분이 추후 셋팅때도 햇갈렸다).
Next.js
하나의 프로젝트에 next.js 를 셋팅해보자. 이미 workspace 는 만들어져 있게 때문에, 따로 설치를 해주면 된다. 공식 홈페이지에서 알려주는데로 설치해보자
npm i --save-dev @nx/next
yarn 을 사용한다면 yarn add 로 설치해주면 된다. 주의할점은 프로젝트니 apps 에 폴더를 만들어 그 내부로 디렉토리를 변경하여 설치하는것이 아니다. root 디렉토리에서 바로 설치해주면 된다.(pakage.json 이 root 에 있으니)
설치가 마무리되면, 이제 apps 폴더에 실제 사용할 내부 repo 를 생성해주자.
npx nx g @nx/next:apps reponame
reponame 부분은 자신이 하고 싶은 이름으로 해주면 된다. next 이기 때문에 2가지정도의 질문이 나올텐데, 첫번째는 어떠한 stylesheet 를 사용할 것인지, 그 다음은 13.4 버전에서 정식 릴리즈 된 app routing 을 적용할 것인지 기존 page 기반으로 할 것인지를 정할 수 있다. 이정도를 정하고 나면 이제 셋팅이 완료가 된다.
여기까지 셋팅을 했다면 기본적인 셋팅은 완료되었다. 좀 더 추가적인 셋팅이 필요하다면, 아래 공식문서를 참고하자
https://nx.dev/packages/next/generators/init
@nx/next:init | Nx
Init Next Plugin.
nx.dev
Tailwindcss
Nx에서 tailwindcss 를 설정하는 방법은 보통 react 기반에서 쉽게 설정할 수 있도록 도와준다. (@nx/react - setup-tailwind)
다만 next.js 에서 설정하는 방법은 공식문서에서 알려주는데로 좀 따로 세팅해주어야 한다. 아래 링크를 참고하면 된다.
https://blog.nrwl.io/setup-next-js-to-use-tailwind-with-nx-849b7e21d8d0
Setup Next.js to use Tailwind with Nx
In the previous article we learned how to set up Next.js in an Nx workspace. In this article, we carry that forward, by adding TailwindCSS…
blog.nrwl.io
우선은 기존 방식대로 root 디렉토리에서 tailwind 를 설치해주자
npm i --save-dev tailwindcss@latest postcss@latest autoprefixer@latest
이후 설치가 완료되었다면,
cd apps/프로젝트
npx tailwindcss init -p
위를 입력하면서 프로젝트 파일에 tailwind.config.js 와 postcss.config.js 파일을 생성해주자. (이후 설정은 위 링크를 따라서 하면 된다. 급한 사람은 바로 위 링크를 참조해서 설치하길 바란다)
이제 tailwind.config.js 의 설정을 좀 변경하기 전에, 생각해볼 것이 있다. apps/프로젝트 에서는 tailwind를 설정해주었으니 당연히 작동하겠으나, 또 다른 프로젝트들이 생성이 되고 그러한 프로젝트에서 같은 설정을 사용하고 싶다면 어떻게 해야할까.
이에 nx 는 preset 을 통해 설정을 root 에서 각 프로젝트로 가져오는 방식을 설명한다. 예를 들어서 tailwind 에서 제공하는 플러그인인 @tailwindcss/typograph 를 공유하고 싶다고 해보자. 매 프로젝트마다 일일히 이러한 플러그인들을 다 설치하는 것 보다 공유해서 쓰는것이 편하고, 이것이 모노레포를 사용하는 이유일테니 세팅을 해주자.
우선 root 에 @tailwindcss/typograph 를 설치하였다고 가정한다. 이제 root 디렉토리에 tailwind-workspace-preset.js 를 생성한 다음 아래와 같이 코드를 작성해주자
// tailwind-workspace-preset.js
module.exports = {
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [require('@tailwindcss/typography')],
};
이제 다시 apps/프로젝트 로 돌아가 tailwind.config.js 에 preset 을 설정해주자
/** @type {import('tailwindcss').Config} */
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
module.exports = {
mode: 'jit',
presets: [require('../../tailwind-workspace-preset.js')],
content: [
'./apps/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
plugins: [],
};
presets 에 root 에 있는 tailwind-workspace-preset.js 를 불러온다. 이제 다른 프로젝트를 세팅하더라도 이렇게 presets 를 이용하면 공통된 설정을 불러올 수 있다.
다시 나머지 tailwind 설정을 마무리해주자. global.css 에 다음과 같이 설정해주자
@tailwind base;
@tailwind components;
@tailwind utilities;
이제 다 됬겠지 하고 실행하면 스타일이 적용이 안될것이다! 왜? 안되는거지 하면서 시간을 엄청 날렸었는데, 중요한 부분은 content 에서 설정하는 경로에 있었다. path의 join 을 활용하여 app 디렉토리 하위에 적용하겠다고 설정해주니, 정상적으로 tailwind 가 작동을 하였다.
/** @type {import('tailwindcss').Config} */
const { join } = require('path');
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
module.exports = {
mode: 'jit',
presets: [require('../../tailwind-workspace-preset.js')],
content: [
join(__dirname, 'app/**/*.{js,jsx,ts,tsx}'), // 예를 주의하자
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {},
},
plugins: [],
};
이제 적당히 tailwind 로 스타일을 조정한 다음 실행해보면 스타일이 적용되어있음을 확인할 수 있다.
아! 참고로 nx 에서 프로젝트를 실행하거나 테스트, 빌드, 린트 검사 하는 방법은 다 공식문서에 적혀있다. 대표적으로 흔히 npm run dev 를 실행하는 방법은
npx nx serve 프로젝트명
이다. 실행하면 포트 4200 으로 실행이 되게 된다. 빌드하는 방법은
npx nx build 프로젝트명
이렇게 하면 된다. test 도 마찬가지다.
Jest & @Testing-liabrary/Jest-Dom
test 언급이 나왔으니, 초기 설정을 해줄 때 test 도 같이 설정해주자. 기본적으로 nx 에서는 workspace 에 자신이 원하는 test 라이브러리를 설정할 수 있도록 해준다. 나는 가장 익숙한 jest 를 선택하였고, 그래서 크게 테스트 부분에 대해 신경써줄 부분은 없고, 필요한 테스트마다 필요한 라이브러리가 있다면 그때그때 설치해주면 된다.
다만 자주 사용하는 toBeInTheDocument() 를 사용하려면 @testing-library/jest-dom 이 필요한데, 매번 테스트 파일마다 위에 이를 import 해주는것은 너무 번거롭다. 이를 해결해보자
jest 도 위 tailwind 와 마찬가지로 공통된 설정은 다른 프로젝트가 공유할 수 있도록 presets 을 지원한다.
// jest.config.js
import { getJestProjects } from '@nx/jest';
export default {
// nx jest executor 를 사용하는 jest config 가 있는 모든 경로를 리스트로 가져온다.
projects: getJestProjects(),
};
// jest.preset.js
const nxPreset = require('@nx/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest',
},
resolver: '@nx/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html'],
};
각 프로젝트에서의 jest.config.js 는 아래와 같이 presets 을 통해 공통된 설정을 부여받는다.
/* eslint-disable */
export default {
displayName: 'web', // 프로젝트의 이름이다.
preset: '../../jest.preset.js', // 이 부분이 preset 받는 부분
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/web', //apps 이하를 커버한다
};
여기서 만약 nx excutor 에서 제공하는 것이 아니라면, 따로 설정을 해주어야 한다. 예를 들면 위에서 언급한 @testing-library/jest-dom.
우선 @testing-library/jest-dom 을 설치해주고, apps/프로젝트 디렉토리에서 setupTest.ts 를 생성해주자
import '@testing-library/jest-dom';
위와 같이 import 를 해준 다음, 다시 jest.config.js 로 돌아와
/* eslint-disable */
export default {
...
setupFilesAfterEnv: ['<rootDir>/setupTest.ts'],
};
위처럼 setupTest.ts 를 지정해주자. 이렇게 설정을 하면, 실제 test 파일에서 이전에는 불러올 수 없었던 toBeInTheDocument() 를 사용할 수 있게 된다. 추가적인 테스트 라이브러리들도 setupTest 에서 설정하면 가능할 것이라 생각한다.
React-Query
redux 를 먼저 셋팅해줄까 고민하다가, 이번 프로젝트에서는 어떻게해서든지 redux 의 사용을 최소화해보려고 한다. 전역 상태의 관리를 redux 에 의존하기보단, react 에서 제공하는 useSelector 나 contextAPI 등을 활용해보고, react-query 역시 그 중 하나의 방안으로 생각했다. 처음 사용해보는 거라 조금 학습 기간이 필요하겠으나, 기존에 SWR 을 사용했었던 것을 기억해보면 어렵지않게 사용할 수 있을 것이라 생각한다.
사용하기 위해서 세팅부터 해주자. Nx 공식문서에는 딱히 tanstack query 에 대한 설명이 나와있지 않았다. next.js 에서 tailwind 를 셋팅했듯이 직접 설치를 해줘야 겠다 생각했다.
npm i @tanstack/react-query
설치가 됬다면 eslint 플러그인도 같이 설치해주자
npm i -D @tanstack/eslint-plugin-query
이제 apps/프로젝트 로 디렉토리를 이동한 다음, next.js 13 버전에 적합하게 react-query 초기 셋팅을 해주도록 하자. 기존과 다르게 app routing 에서는 _app.tsx 가 따로 존재한다기 보단, 각 page 마다 layout 을 적용할 수 있다. 그리고 하부 디렉토리에서 위로 타고 올라오면서 상위 layout 을 참고하는 형식인데, 이를 활용해서 맨 위 디렉토리의 layout.tsx 에서 reactQuery 설정을 해주자.
// app/ReactQueryProvider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export const ReactQueryProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
// app/layout.tsx
import './global.css';
// provider 를 설정해준다.
import { ReactQueryProvider } from './ReactQueryProvider';
export const metadata = {
title: 'Welcome to web',
description: 'Generated by create-nx-workspace',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ReactQueryProvider>
<html lang="en">
<body>{children}</body>
</html>
</ReactQueryProvider>
);
}
Provider 셋팅을 마무리하고 실제 잘 작동하는지 테스트를 해봤을 때, 예상한대로 잘 작동함을 확인할 수 있었다.
// app/query/page.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
export default function Query() {
const { data, isLoading } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: () =>
fetch('https://jsonplaceholder.typicode.com/todos').then((data) =>
data.json()
),
});
if (isLoading || !data) {
return <div className="text-tl text-cyan-500">Loading...</div>;
}
return (
<main className="flex flex-col h-100% items-center justify-center">
<h1 className="text-xl">Todos</h1>
<div className="flex flex-col gap-2">
{data?.map((todo) => (
<div className="flex" key={todo.id}>
<h2>{todo.title}</h2>
<p>{todo.completed ? 'ok' : 'X'}</p>
</div>
))}
</div>
</main>
);
}
이번 포스팅의 목표는 사실 나 자신이 지금까지 셋팅하였던 과정을 까먹지(?) 않게 하기 위한 포스팅이었다. 그래서 조금 설명적으로 부족한 부분들이 있겠지만, 그냥 이런식으로 설정을 하는것이고 다른 설정을 할 때 어떠한 방향성을 참고하고 읽어주면 좋겠다.
결국 이번 사이드 프로젝트에는 굳이 모노레포 셋팅이 필요없지만, 결국은 이 모든것들이 또하나의 공부 과정이 될 테니 꾸준히 셋팅도 하고 프로젝트도 진행해 나가야겠다. 조금 더 사용하고, 더 포스팅할 내용이 있다면 포스팅을 이어보도록 하겠다.