항상 도구나 테스트를 필요할 때 Playground에 작성하곤 했습니다. 이 저장소는 Openlayers Examples의 기본 골격을 바탕으로 작성하였는데, Openlayer의 업데이트에 맞춰 따라가지를 못하고 그렇다고 계속 유지 보수 해가며 사용하기에는 이런저런 불편함이 있었습니다. 덕분에 Github Security에는 항상 몇 개의 경고가 노출되고 있었지만, 다른 대안이 없어서 애써 무시하며 사용하고 있었습니다.
그러다 최근에 새로운 도구를 작성하고 싶다는 생각이 들어 이를 어떻게 해결할까 하다 자유롭게 사용할 수 있고 유지 보수도 용이하게 자주 사용하고 있는 기술 스택으로 직접 구성하는 쪽으로 방향을 잡았습니다.
사용할 기술 스택은 근래 필자가 자주 사용하는 TypeScript, Next.js, Ant Design을 사용하기로 생각하였습니다. Gatsby도 고려 대상이었지만 사이트 특성상 서버 통신이 필요한 경우 Next.js가 사용에 좀 더 용이해 보여 이쪽으로 선정했습니다.
코드를 배포할 공간은 비용적인 측면을 생각해봤을 때, 기존에 블로그 및 개인 프로젝트에 사용하고 있던 Firebase를 사용하고 싶었습니다. 이전에 찾아봤을 때 정적 호스팅만 지원해 주는 것으로 기억하고 있었는데, 이번에 다시 한번 찾아보니 Firebase Function을 통한 동적 호스팅이 가능한 것으로 보여 이를 사용해보기로 마음을 먹고 프로젝트를 구성했습니다.
Next.js 설정은 Getting Started 글과 TypeScript 글을 참고하여 기본 프로젝트를 생성하였습니다. 이후 Next.js 저장소의 예제 중 with Firebase Hosting example을 참고하여 진행하였습니다.
이 예제에서는 Next.js의 기본 구성과 달리 pages
가 src
폴더 아래에 위치하도록 작성되어 있습니다. 이 부분은 필자도 Firebase의 코드들과 위치를 분리하고 싶어 차용하여 진행하였습니다. 이 예제는 pages
를 src
폴더 아래에 위치하도록 변경할 뿐만 아니라 package.json
에서 next
가 src
를 바라보도록 처리하였습니다. 이를 참고하여 필자도 next.config.js
와 tsconfig.json
을 src
폴더 아래에 위치시켰습니다.
{
"scripts": {
"dev": "next src/",
"build": "next build src/",
"start": "next start src/"
},
}
module.exports = {
distDir: '../.next',
}
const isDev = process.env.NODE_ENV !== 'production'
const nextjsDistDir = join('src', require('./src/next.config.js').distDir)
const nextjsServer = next({
dev: isDev,
conf: {
distDir: nextjsDistDir,
},
})
const nextjsHandle = nextjsServer.getRequestHandler()
작성하면서 이 부분에서 Next.js의 빌드 파일인 .next
와 tsconfig.json
의 baseUrl
, next.config.js
내의 config 파일들에 대한 설정이 꼬였습니다. next
에 conf
를 통해 직접 설정을 하면 sass, less, image 등의 다른 설정이 되지 않고, 설정을 하지 않을 경우 distDir
위치가 찾지 못하는 등의 문제가 계속 발생하습니다.
이 문제는 src Directory라는 글을 참고하니 next.config.js
와 tsconfig.json
등의 파일들은 건드리지 않고 pages
만 src
폴더의 아래로 옮기면 됨을 확인하고 이에 맞춰 설정을 하니 해당 문제는 해결되었습니다.
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
...
},
}
const plugins = withPlugins(
[
...other plugins,
],
{
distDir: '.next',
},
);
module.exports = plugins;
const isDev = process.env.NODE_ENV !== 'production'
const nextjsServer = next({
dev: isDev,
});
const nextjsHandle = nextjsServer.getRequestHandler();
다른 로직들이 함께 있어 작업한 저장소가 비공개이지만, 기본 예제 기준으로 하면 상단과 같은 모습이 될 작성될 것으로 보입니다.
이 부분은 필요에 따라 생략해도 될 것으로 보입니다. 필자는 인증에 관련해서 Mobx에 인증받은 유저의 정보를 초기화하여, 헤더 영역의 로그인 정보가 깜박이지 않도록 하기 위해서 Next.js에 express
를 추가하였습니다. 추가적으로 Firebase Function에 Next.js를 연동하기 위해서 비슷한 작업이 필요해서 작업 환경과 Firebase의 환경을 동일하게 가져갈 수 있는 이점도 있었습니다. Next.js에 이에 대한 설정은 Custom Sever 문서를 참고하였습니다.
{
"main": "index.js",
"scripts": {
"dev": "PHASE=dev node local.js",
"build": "next build",
"start": "next start",
...
},
}
const next = require('next');
const express = require('express');
const dev = process.env.PHASE === 'dev';
const app = next({
dev,
});
const handle = app.getRequestHandler();
const prepare = app.prepare();
const server = express();
server.get('*', async (req, res) => {
res.set(`Cache-Control', 'public, max-age=${60 * 60 * 12}, s-maxage=${60 * 60 * 24}`);
await prepare;
await handle(req, res);
});
module.exports = server;
const server = require('./server');
server.listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
const {https} = require('firebase-functions');
const server = require('./server');
exports.nextjs = {
server: https.onRequest(server),
};
대략적인 구성은 상단과 같이 공통 로직인 server.js
를 가지고 작업 환경에서는 local.js
를 참조하여 실행하고 Firebase에서는 index.js
를 참조하여 실행합니다.
Firebase에 대한 설정은 자바스크립트 프로젝트에 Firebase 추가라는 글을 참고 해서 프로젝트를 생성하고, Node와 연결할 부분만 숙지하면 됩니다. 이후 필요에 따라 호스팅에 추가 도메인을 설정하여도 되고 필요하지 않다면 생략하셔도 무방할 것 같습니다.
이때 한가지 문제에 봉착하게 됩니다. 무료로 사용할 수 있는 Spark 요금제에서 Firebase Function은 Node 8을 사용해야 하는데, Firebase Function에서 depricated 됬을 뿐만 아니라 Next.js의 최신 버전들을 사용하지 못하는 문제가 있습니다. 이에 필자는 요금제를 Blaze로 변경 후 사용하였습니다(이때 그만 두었어야 했는데…). 아직 변경한지 얼마되지 않아 확실치 않지만, Blaze로 변경하여도 무료 할당 부분까지는 과금이 되지 않는 것으로 보입니다.
{
"projects": {
"default": "<project-name-here>"
}
}
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "nextjs-server"
}
]
},
"functions": {
"source": ".",
"predeploy": [
"npm --prefix \"$PROJECT_DIR\" install",
"npm --prefix \"$PROJECT_DIR\" run build"
]
}
}
{
"main": "index.js",
"scripts": {
"dev": "PHASE=dev node local.js",
"build": "next build",
"start": "next start",
"serve": "npm run build && firebase emulators:start --only functions,hosting",
"shell": "npm run build && firebase functions:shell",
"deploy": "firebase deploy --only functions,hosting",
"logs": "firebase functions:log"
},
}
Firebase 프로젝트가 생성되었다면 생성한 환경에 맞추어 상단과 같이 .firebaserc
와 firebase.json
를 설정하고 package.json
에 script
를 추가하면 됩니다.
다만 상단과 같이 설정을 하면 작업 환경에서 emulator로 확인을 하면 Next.js에서 basePath
를 잘 못 가져오는 이슈가 발생합니다. 필자는 해당 부분을 우회하기 위해 아래와 같이 serve
에서만 별도 설정 값으로 빌드되도록 변경하였습니다.
const test = process.env.PHASE === 'test';
const plugins = withPlugins(
[
...other plugins,
],
{
basePath: !test ? '' : '/<project-name-here>/us-central1/labs-server',
distDir: '.next',
},
);
module.exports = plugins;
{
"main": "index.js",
"scripts": {
"dev": "PHASE=dev node local.js",
"build": "next build",
"start": "next start",
"preserve": "PHASE=test npm run build",
"serve": "firebase emulators:start --only functions,hosting",
"shell": "firebase functions:shell",
"deploy": "firebase deploy --only functions,hosting --non-interactive",
"logs": "firebase functions:log"
},
}
앞서 언급했던 코드들을 정리해서 사용하면 아래와 같은 코드들을 사용하면 될 것 같습니다.
{
"projects": {
"default": "<project-name-here>"
}
}
{
"hosting": {
"public": "public",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"function": "nextjs-server"
}
]
},
"functions": {
"source": ".",
"predeploy": [
"npm --prefix \"$PROJECT_DIR\" install",
"npm --prefix \"$PROJECT_DIR\" run build"
]
}
}
const test = process.env.PHASE === 'test';
const plugins = withPlugins(
[
...other plugins,
],
{
basePath: !test ? '' : '/<project-name-here>/us-central1/labs-server',
distDir: '.next',
},
);
module.exports = plugins;
{
"main": "index.js",
"scripts": {
"dev": "PHASE=dev node local.js",
"build": "next build",
"start": "next start",
"preserve": "PHASE=test npm run build",
"serve": "firebase emulators:start --only functions,hosting",
"shell": "firebase functions:shell",
"deploy": "firebase deploy --only functions,hosting --non-interactive",
"logs": "firebase functions:log"
},
}
const next = require('next');
const express = require('express');
const dev = process.env.PHASE === 'dev';
const app = next({
dev,
});
const handle = app.getRequestHandler();
const prepare = app.prepare();
const server = express();
server.get('*', async (req, res) => {
res.set(`Cache-Control', 'public, max-age=${60 * 60 * 12}, s-maxage=${60 * 60 * 24}`);
await prepare;
await handle(req, res);
});
module.exports = server;
const server = require('./server');
server.listen(3000, err => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
const {https} = require('firebase-functions');
const server = require('./server');
exports.nextjs = {
server: https.onRequest(server),
};
이 글을 작성하는 시점에서 Firebase Function을 통해 배포해서 동작하는 것까지 확인을 하였지만, 이를 사용할 만한 수준인지 여부가 궁금하시다면 단호하게 “아니다” 라고 말할 것 같습니다.
개인용 툴 사이트라는 것을 감안해도 초기 로딩 속도가 30초가량 소요되어 사용하기에는 무리가 있어 보입니다. 게다가 Firebase 동적 호스팅을 사용하기 위해서는 us-central1 리전만 사용이 가능하기 때문에 다른 대안을 찾아보는 것이 정신 건강에 좋을 것 같습니다.