Outsider's Dev Story: node.js 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T10:24:37+09:00Textcube 1.10.7 : Tempo primoPrisma 클라이언트 설정 파악하기Outsiderhttps://blog.outsider.ne.kr/16172022-08-30T03:37:16+09:002022-08-30T03:37:16+09:00<p><a href="https://blog.outsider.ne.kr/1614">Node.js/TypeScript용 ORM Prisma 살펴보기</a>에서 Prisma의 기본적인 기능을 살펴봤다. 일반적인 웹 애플리케이션 서버라면 이후에는 Prisma 클라이언트를 이용해서 쿼리를 사용하면 되지만 나 같은 경우는 Prisma 클라이언트를 다수 만들고 싶었다. 보통 데이터베이스 종류에 상관없이 사용할 데이터베이스는 한 가지만 사용하겠지만 내가 Prisma를 도입한 프로젝트는 사용자가 원하는 데이터베이스를 선택해서 사용하게 하고 싶었기 때문에 지원하는 데이터베이스를 늘려가면서 여러 타입의 데이터베이스를 지원해야 했기에 클라이언트도 여러 클라이언트가 필요했다.</p>
<p>보일러플레이트로 만들어진 코드 외에 파일의 위치나 구조를 바꾸다 보니 좀 더 Prisma의 구조에 관해 알게 되었다.<br />
<br></p>
<h1>임의 위치의 스키마 파일</h1>
<p><code>postgresql/db.schema</code> 파일을 만들어서 이전 글에서도 본 간단한 스키마 파일을 생성해 보자. 일부러 기본 위치인 <code>prisma/schema.prisma</code>가 아닌 임의의 파일명을 사용했다.</p>
<pre class="line-numbers"><code class="language-clike">// postgresql/db.schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
</code></pre>
<p>이제 이전처럼 마이그레이션을 시도하면 당연히 <code>schema.prisma</code> 파일을 찾을 수 없다고 오류가 발생한다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/prisma migrate dev --name init
Error: Could not find a schema.prisma file that is required for this command.
You can either provide it with --schema, set it as `prisma.schema` in your package.json or put it into the default location ./prisma/schema.prisma https://pris.ly/d/prisma-schema-location
</code></pre>
<p>기본 위치가 아니기 때문에 못 찾는 것이므로 <code>--schema</code> 옵션으로 스키마 파일을 지정해 주면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/prisma migrate dev --name init --schema postgresql/db.schema
Prisma schema loaded from postgresql/db.schema
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
Applying migration `20220829163109_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20220829163109_init/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
added 2 packages, and audited 5 packages in 3s
found 0 vulnerabilities
✔ Generated Prisma Client (4.2.1 | library) to ./node_modules/@prisma/client in 45ms
</code></pre>
<p>이때 생성되는 <code>migrations</code> 폴더도 스키마 파일 옆에 생성된다. 그래서 여기서는 <code>postgresql/migrations</code>에 생성된다. <code>--schema</code> 옵션은 <code>prisma generate</code>로 마이그레이션 없이 Prisma 클라이언트를 생성할 때도 사용할 수 있다.<br />
<br></p>
<h1>Prisma 클라이언트</h1>
<p>처음 <code>npm install prisma</code>로 설치하면 아래처럼 설치가 된다.</p>
<pre class="line-numbers"><code class="language-bash">node_modules
├── @prisma
│ └── engines
└── prisma
├── LICENSE
├── README.md
├── build
├── install
├── package.json
├── preinstall
├── prisma-client
└── scripts
</code></pre>
<p><code>./node_modules/.bin/prisma generate --schema postgresql/db.schema</code>로 클라이언트를 생성하면 <code>package.json</code>에도 <code>"@prisma/client</code>가 추가되고 <code>node_modules</code>에도 추가 파일이 생성된다.</p>
<pre class="line-numbers"><code class="language-bash">node_modules
├── .prisma
│ └── client
├── @prisma
│ ├── client
│ ├── engines
│ └── engines-version
└── prisma
├── LICENSE
├── README.md
├── build
├── install
├── libquery_engine-darwin-arm64.dylib.node
├── package.json
├── preinstall
├── prisma-client
└── scripts
</code></pre>
<p><code>.prisma/client</code>, <code>@prisma/client</code>, <code>@prisma/engines-version</code>이 추가되고 <code>prisma</code> 아래도 <code>libquery_engine-darwin-arm64.dylib.node</code>가 추가되었다.</p>
<p>처음 사용할 때 클라이언트를 생성하면 <code>node_modules</code>아래 추가되는 게 좀 어색하게 느껴졌다. 처음에는 클라이언트가 생성되는 구조를 잘 몰랐기 때문에 내가 설치하지도 않은 패키지를 임포트해서 사용하면서 왜 동작하는지도 의아했다. <code>package.json</code>에 추가해 주는 것도 이러한 어색함을 줄여주기 위함으로 보인다.</p>
<p>하지만 이렇게 사용할 경우 <code>node_modules/@prisma/client</code>에 클라이언트가 설치되기 때문에 앞에서 얘기했듯이 여러 클라이언트를 만들어야 한다면 사용할 수 없다.<br />
<br></p>
<h1>임의 위치의 Prisma Client</h1>
<p>클라이언트를 다른 곳에 생성하려면 스키마 파일의 <code>generator</code>에서 <code>output</code>을 지정해 주면 된다. 여기서 상대 경로는 스키마 파일의 위치를 기준으로 상대 경로가 된다.</p>
<pre class="line-numbers"><code class="language-clike">// postgresql/db.schema
generator client {
provider = "prisma-client-js"
output = "./client"
}
</code></pre>
<p>이렇게 하고 클라이언트를 생성하면 <code>node_modules/@prisma/client</code>는 추가되지만 실제 사용하는 클라이언트는 <code>postgresql/client</code> 위치에 설치된다.</p>
<pre class="line-numbers"><code class="language-bash">postgresql/client
├── index-browser.js
├── index.d.ts
├── index.js
├── libquery_engine-darwin-arm64.dylib.node
├── package.json
├── runtime
│ ├── edge.js
│ ├── index-browser.d.ts
│ ├── index-browser.js
│ ├── index.d.ts
│ └── index.js
└── schema.prisma
</code></pre>
<p>이제 아래처럼 <code>node_modules</code>아래서가 아니라 클라이언트 위치에서 임포트해서 Prisma 클라이언트를 생성할 수 있다.</p>
<pre class="line-numbers"><code class="language-js">const { PrismaClient } = require('./postgresql/client');
</code></pre>
<p><br></p>
<h1>Prisma 엔진</h1>
<p>이때쯤 Prisma 엔진의 존재를 알아야 하는데 Prisma가 <code>node_modules</code> 아래 클라이언트를 생성하는 것 이유가 있어 보인다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/2105687680.jpg" width="600" height="335" alt="Typical flow of the query engine at run time" title="" /><br>출처: <a href="https://www.prisma.io/docs/concepts/components/prisma-engines/query-engine#the-query-engine-at-runtime">The query engine at runtime 문서</a></p>
<p>내가 일반적으로 쓰던 ORM과 달리 Prisma에는 엔진이라는 중간 서버가 있다. 그래서 Prisma 클라이언트가 바로 데이터베이스에 접속하는 게 아니라 처음 실행될 때 바이너리로 된 Prisma 엔진(쿼리 엔진)이 실행되고 클라이언트는 HTTP로 Prisma 엔진에 요청을 보내면 엔진이 SQL을 만들어서 데이터베이스에 질의하고 그 결과를 다시 클라이언트로 보내준다.</p>
<p>클라이언트를 생성했을 때 만들어진 <code>libquery_engine-darwin-arm64.dylib.node</code> 파일이 쿼리 엔진이고 이 파일은 클라이언트가 <a href="https://nodejs.org/api/n-api.html">Node-API</a>로 불러온다.</p>
<pre class="line-numbers"><code class="language-js">// connect.js
const { PrismaClient } = require('./postgresql/client');
const prisma = new PrismaClient();
prisma.$connect()
.then(() => {
console.log('connected');
})
.catch((err) => {
console.log(err);
});
</code></pre>
<p>아래 JavaScript 파일로 데이터베이스 연결을 테스트해보자. <code>DEBUG=*</code> 환경변수를 지정해서 <code>prisma</code>의 디버그 로그를 켰다.</p>
<pre class="line-numbers"><code class="language-bash">$ DEBUG=* node connect.js
prisma:tryLoadEnv Environment variables not found at null +0ms
prisma:tryLoadEnv Environment variables not found at undefined +0ms
prisma:tryLoadEnv No Environment variables loaded +0ms
prisma:tryLoadEnv Environment variables not found at null +1ms
prisma:tryLoadEnv Environment variables not found at undefined +0ms
prisma:tryLoadEnv No Environment variables loaded +0ms
prisma:client dirname /Users/outsider/prisma-demo/postgresql/client +0ms
prisma:client relativePath .. +0ms
prisma:client cwd /Users/outsider/temp/prisma-demo2/postgresql +0ms
prisma:client clientVersion 4.2.1 +0ms
prisma:client clientEngineType library +0ms
prisma:client:libraryEngine internalSetup +0ms
prisma:client:libraryEngine:loader Searching for Query Engine Library in /Users/outsider/prisma-demo/.prisma/client +0ms
prisma:client:libraryEngine:loader Searching for Query Engine Library in /Users/outsider/prisma-demo/postgresql/client +0ms
prisma:client:libraryEngine:loader loadEngine using /Users/outsider/prisma-demo/postgresql/client/libquery_engine-darwin-arm64.dylib.node +1ms
prisma:client:libraryEngine library starting +6ms
prisma:info Starting a postgresql pool with 21 connections.
prisma:client:libraryEngine library started +34ms
connected
prisma:client:libraryEngine:exitHooks exit event received: beforeExit +0ms
prisma:client:libraryEngine:exitHooks exit event received: exit +1ms
</code></pre>
<p>위 로그를 살펴보면 엔진이 로딩되는 것을 볼 수 있다. 중요한 부분을 보면 아래와 같은데 엔진 타입은 <code>library</code>이고 엔진 파일인 <code>libquery_engine-darwin-arm64.dylib.node</code>로 엔진을 불러온 것을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">prisma:client clientEngineType library
prisma:client:libraryEngine:loader loadEngine using /Users/outsider/prisma-demo/postgresql/client/libquery_engine-darwin-arm64.dylib.node +1ms
</code></pre>
<p>Node-API 대신 실행할 수 있는 바이너리로 엔진을 실행할 수도 있다. 이때는 스키마에서 <code>engineType = "binary"</code>을 지정하면 된다.</p>
<pre class="line-numbers"><code class="language-clike">// postgresql/db.schema
generator client {
provider = "prisma-client-js"
output = "./client"
engineType = "binary"
}
</code></pre>
<p>이 설정으로 클라이언트를 생성하면 <code>libquery_engine-darwin-arm64.dylib.node</code> 대신 <code>query-engine-darwin-arm64</code>가 생성된다. 다시 연결 테스트를 해보자.</p>
<pre class="line-numbers"><code class="language-bash">$ DEBUG=prisma:* node connect.js
prisma:tryLoadEnv Environment variables not found at null +0ms
prisma:tryLoadEnv Environment variables not found at undefined +0ms
prisma:tryLoadEnv No Environment variables loaded +0ms
prisma:tryLoadEnv Environment variables not found at null +1ms
prisma:tryLoadEnv Environment variables not found at undefined +0ms
prisma:tryLoadEnv No Environment variables loaded +0ms
prisma:client dirname /Users/outsider/prisma-demo/postgresql/client +0ms
prisma:client relativePath .. +0ms
prisma:client cwd /Users/outsider/prisma-demo/postgresql +0ms
prisma:client clientVersion 4.2.1 +0ms
prisma:client clientEngineType binary +0ms
prisma:engine { cwd: '/Users/outsider/prisma-demo/postgresql' } +0ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/.prisma/client +1ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/postgresql/client +0ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/.prisma/client +0ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/postgresql/client +0ms
prisma:engine {
flags: [
'--enable-raw-queries',
'--enable-metrics',
'--enable-open-telemetry',
'--port',
'51376'
]
} +9ms
prisma:engine stdout Starting a postgresql pool with 21 connections. +409ms
prisma:engine stdout Started query engine http server on http://127.0.0.1:51376 +29ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/.prisma/client +7ms
prisma:engine Search for Query Engine in /Users/outsider/prisma-demo/postgresql/client +0ms
connected
prisma:engine Client Version: 4.2.1 +6ms
prisma:engine Engine Version: query-engine 2920a97877e12e055c1333079b8d19cee7f33826 +0ms
prisma:engine Active provider: postgresql +0ms
</code></pre>
<p>아까와 달리 <code>clientEngineType binary</code>로 지정된 것을 알 수 있고 <code>prisma:engine</code> 로그가 다수 출력된 것을 볼 수 있다. 별도의 바이너리로 실행되었기 때문에 HTTP 서버로 실행되어서 클라이언트와 데이터베이스 사이에 통신 역할을 한다.</p>
<p>이 쿼리 엔진은 커넥션 풀을 이용해서 데이터베이스의 연결을 관리하고 클라이언트에서 쿼리 요청받으면 SQL을 생성해서 데이터베이스에 SQL을 질의하고 받은 응답을 다시 클라이언트에 보내준다. 문서를 좀 봤는데도 엔진에서 library 타입과 binary 타입의 장단점을 잘 모르겠다.</p>
<p>앞에서 Prisma 클라이언트가 <code>node_modules</code>에 만들어지는 이유를 알 것 같다고 했는데 아마도 이 엔진 때문일 것으로 생각한다. 스키마를 기반으로 만들어진 클라이언트는 스키마에 묶이기 때문에 언제든 재생성할 수 있지만 Git 같은 버전 관리에 넣는다고 해도 크게 문제 될 것은 없어 보인다.(재생성할 수 있으니 굳이 넣지 않을 뿐...) 하지만 엔진 바이너리인 <code>libquery_engine-darwin-arm64.dylib.node</code>같은 경우 파일명에서 알 수 있듯이 macOS ARM64용 파일이다. 로컬에서 macOS로 개발하고 이를 Docker에 넣어서 Linux에서 실행한다면 당연히 엔진이 실행되지 않을 것이다. <code>node_modules</code>는 버전 관리에 넣지 않는 것이 일반적이므로 일부러 여기 넣어서 각 배포환경에서 매번 클라이언트를 생성하도록 하려는 의도일 것이고 이 글에서 보았듯이 다른 곳에 클라이언트를 생성하게 설정했다고 하더라도 바이너리 때문에 매번 생성하는 것이 바람직해 보인다.</p>
<pre class="line-numbers"><code class="language-clike">// postgresql/db.schema
generator client {
provider = "prisma-client-js"
output = "./client"
binaryTargets = ["darwin", "darwin-arm64", "windows", "debian-openssl-1.1.x", "linux-musl"]
}
</code></pre>
<p>특별한 이유로 다양한 플랫폼에 바이너리를 생성해야 한다면 위처럼 <code>binaryTargets</code>에 지정하면 된다. 지원하는 타깃은 <a href="https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#binarytargets-options">문서</a>에서 확인할 수 있다.</p>
<p>최근에 다양한 테스트를 해보면서 약간의 Prisma 구조는 파악했지만, 아직도 모르는 부분이 더 많다. 이것 때문에 고생해서 그런지 몰라도 왜 엔진을 별도로 바이너리로 제공하는지도 약간 의문이기도 하다. 써보다 보면 좀 더 파악되지 않을까 싶다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1617?commentInput=true#entry1617WriteComment">댓글 쓰기</a></strong></p>소스에서 pkg로 패키징되었는지 확인하기Outsiderhttps://blog.outsider.ne.kr/16162022-08-25T00:12:53+09:002022-08-25T00:12:53+09:00<p><a href="https://github.com/vercel/pkg">pkg</a>는 <a href="https://blog.outsider.ne.kr/1379">이전에도 소개</a>한 적이 있는데 Node.js 프로그램을 바로 실행할 수 있는 하나의 바이너리로 만들어 주는 프로그램이다. 나는 CLI처럼 사용자도 Node.js가 설치되는 것을 가정할 수 없을 때 종종 사용하고 있다. Node.js까지 패키징되기 때문에 용량은 좀 크지만 그래도 얻을 수 있는 편의성이 있다고 생각한다.</p>
<p>pkg로 패키징할 프로그램을 작성하다 보니 소스 코드에서 pkg 내에서 실행되고 있는지 그냥 <code>node</code>로 실행되고 있는지 구분할 필요가 있었다. 배포하면 사용자는 pkg로 패키징된 프로그램을 쓰겠지만 개발할 때는 편의상 로컬에서 Node.js로 바로 코드를 실행하는데 (내 경우에서는) 로그를 다른 식으로 출력하고 싶었다. 개발할 때는 개발에 유리하게 로그를 남기지만 pkg된 후에는 사용자가 편한 로그를 남기고 싶었다.</p>
<p>그러다 보니 실행될 때 지금 패키징된 상태인지 구분할 필요가 있어서 방법을 찾아봤다.<br />
<br></p>
<h1>간단한 예제 프로그램</h1>
<p>일단 예제용 node.js 프로그램을 만들기 위해 <code>cli.js</code> 파일을 만들었다. 이 코드는 <code>console.log()</code>로 메시지만 출력한다.</p>
<pre class="line-numbers"><code class="language-js">// cli.js
console.log('This is CLI');
</code></pre>
<p>pkg에 실행파일의 엔트리 포인트를 알려줘야 하므로 <code>package.json</code>에 <code>bin</code>을 지정한다.</p>
<pre class="line-numbers"><code class="language-json">{
"name": "pkg-example",
"version": "1.0.0",
"bin": "cli.js"
}
</code></pre>
<p>이 간단한 프로그램을 <code>pkg</code>로 패키징 해보자. 보통은 <code>pkg</code>를 의존성에 추가하고 <code>npm script</code>로 빌드 명령어를 추가하겠지만 여기선 간단하게 <code>npx</code>로 <code>pkg</code>를 실행했다.</p>
<pre class="line-numbers"><code class="language-bash">$ npx pkg .
> pkg@5.8.0
> Targets not specified. Assuming:
node16-linux-arm64, node16-macos-arm64, node16-win-arm64
</code></pre>
<p>따로 옵션을 주지 않았기 때문에 Linux, macOS, Windows용이 만들어졌다.(Apple M1을 쓰고 있어서 arm64로 빌드되었다) 아래처럼 <code>pkg-example-linux</code>, <code>pkg-example-macos</code>, <code>pkg-example-win.exe</code> 3개의 바이너리가 생성되었다.</p>
<pre class="line-numbers"><code class="language-bash">├── cli.js
├── package.json
├── pkg-example-linux
├── pkg-example-macos
└── pkg-example-win.exe
</code></pre>
<p>macOS를 쓰고 있어서 macOS용 바이너리를 실행하면 앞에서 작성한 메시지가 출력되는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./pkg-example-macos
This is CLI
</code></pre>
<p><br></p>
<h1>pkg로 패키징되었는지 구분하기</h1>
<p>구분할 수 있게 내부에 환경변수를 주입해 주려나 했는데 그렇지는 않았고 Node.js의 <a href="https://nodejs.org/dist/latest-v16.x/docs/api/process.html"><code>process</code></a>를 확인하면 기존 Node.js로 실행했을 때와 다른 부분을 볼 수 있었다.</p>
<pre class="line-numbers"><code class="language-json">process {
versions: {
...
pkg: '5.8.0'
},
title: './pkg-example-macos',
pkg: {
mount: [Function: createMountpoint],
entrypoint: '/snapshot/pkg-example/cli.js',
defaultEntrypoint: '/snapshot/pkg-example/cli.js',
path: { resolve: [Function: resolve] }
},
...
}
</code></pre>
<p><code>process</code> 객체에서 다른 부분만 적었다. <code>process.versions.pkg</code>에 버전이 들어가 있어서 사용된 <code>pkg</code> 버전을 알 수 있었고 <code>process.title</code>에 실행한 프로그램 이름이 들어 있었다. <code>node cli.js</code>로 실행한 경우에는 <code>process.title</code>에 <code>node</code>가 들어 있었다. 하지만 <code>title</code>은 사용자가 얼마든지 바꿀 수 있어서 신뢰할 수 없었다. <code>process.pkg</code>에는 <code>pkg</code>가 몇 가지 정보와 함수를 넣어주고 있다.</p>
<p><code>process.versions.pkg</code>도 가능은 하겠지만 <code>process.pkg</code>가 더 확실하고 간결해서 <code>process.pkg</code>를 사용했다.</p>
<pre class="line-numbers"><code class="language-clike">// cli.js
if (process.pkg) {
console.log('Run as packaged');
} else {
console.log('Run by Node.js');
}
</code></pre>
<p>예제라서 <code>process.pkg</code>로 분기했다.</p>
<pre class="line-numbers"><code class="language-bash">$ node cli.js
Run by Node.js
$ ./pkg-example-macos
Run as packaged
</code></pre>
<p>패키징되었을 때와 아닐 때를 잘 구분해서 실행되는 것을 볼 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1616?commentInput=true#entry1616WriteComment">댓글 쓰기</a></strong></p>Node.js/TypeScript용 ORM Prisma 살펴보기Outsiderhttps://blog.outsider.ne.kr/16142022-08-15T19:39:42+09:002022-08-15T19:35:32+09:00<p>데이터베이스를 다루는 프로젝트를 하려면 ORM(혹은 SQL 매퍼)같은 라이브러리가 필수다. SQL을 직접 다뤄도 되기는 하지만 쓰기도 어렵고 유지보수도 어렵기 때문에 ORM 같은 라이브러리를 쓰는 게 일반적이다. JavaScript로 프로젝트를 하면서 <a href="https://sequelize.org/">Sequelize</a>나 <a href="https://bookshelfjs.org/">Bookshelf.js</a>도 써봤고 MongoDB를 쓸 때는 <a href="https://mongoosejs.com/">mongoose</a>또 사용해봤다.</p>
<p>최근 몇 년간에는 JavaScript로 데이터베이스를 크게 다룰 일이 많지 않아서 사실 ORM을 써본 지가 오래되었고 간단한 토이 프로젝트에서는 쉽게 사용할 수 있는 <a href="https://github.com/louischatriot/nedb">nedb</a>나 <a href="https://pouchdb.com/">pouchdb</a>같은 JavaScript 데이터베이스를 더 많이 사용했다. 간단히 저장했다 꺼내 쓸 수 있으면서 성능을 걱정할 만큼 많은 데이터가 필요한 건 아니라서 그냥 쓰고 있었는데 현재 일하는 프로젝트에서 데이터베이스를 다루기 위해서 동료가 <a href="https://www.prisma.io/">Prisma</a>를 도입해서 실제로 사용해 보게 되었고 나쁘진 않은 것 같아서 (공부 겸) 내 사이드 프로젝트에도 도입하고 있다. (TypeORM 등 다양한 대안과 내가 세밀하게 비교해 본 것은 아니다.)<br />
<br></p>
<h1>Prisma ORM</h1>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/2263242473.jpg" width="750" height="297" alt="Prisma 홈페이지" title="" /></p>
<p>사이트에 나온 대로 Prisma는 Node.js와 TypeScript용 ORM으로 현재 PostgreSQL, MySQL, MiariaDB, SQL Server, SQLite, CockroachDB, MongoDB 등 다양한 데이터베이스를 지원하고 있다. Prisma에서 데이터 플랫폼을 유료로 서비스하고 있지만 <a href="https://github.com/prisma/prisma">Prisma ORM</a> 자체는 오픈소스라서 무료로 이용하고 있다. 데이터 플랫폼은 안써봤지만 설명을 보면 협업을 도와주는 기능과 서비스 프락시 등을 제공하는 걸로 보인다.<br />
<br></p>
<h2>Prisma 프로젝트 설정</h2>
<p>Node.js와 TypeScript 모두에서 사용할 수 있지만 여기서는 TypeScript를 기준으로 사용해 보자. 일단 <code>package.json</code>이 있는 TypeScript 프로젝트에 TypeScript 의존성을 추가하자.</p>
<pre class="line-numbers"><code class="language-base">$ npm install typescript ts-node @types/node --save-dev
</code></pre>
<p>TypeScript 컴파일을 위한 <code>tsconfig.json</code> 파일을 다음이 내용을 추가한다.</p>
<pre class="line-numbers"><code class="language-json">{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
</code></pre>
<p>이제 <code>prisma</code> 의존성을 설치한다.</p>
<pre class="line-numbers"><code class="language-bash">$ npm install prisma --save-dev
</code></pre>
<p>설치된 바이너리는 <code>./node_modules/.bin/prisma</code>에 있으므로 버전 등을 확인해 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/prisma -v
prisma : 4.2.1
@prisma/client : Not found
Current platform : darwin-arm64
Query Engine (Node-API) : libquery-engine 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node)
Migration Engine : migration-engine-cli 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/migration-engine-darwin-arm64)
Introspection Engine : introspection-core 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/introspection-engine-darwin-arm64)
Format Binary : prisma-fmt 2920a97877e12e055c1333079b8d19cee7f33826 (at node_modules/@prisma/engines/prisma-fmt-darwin-arm64)
Default Engines Hash : 2920a97877e12e055c1333079b8d19cee7f33826
Studio : 0.469.0
</code></pre>
<p>이제 Prisma를 초기화해보자. <code>prisma init</code> 명령어로 초기화를 하는 데 테스트로는 PostgreSQL을 사용했다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/prisma init --datasource-provider postgresql
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
</code></pre>
<p>위 안내메시지처럼 <code>prisma/schema.prisma</code>라는 스키마 파일이 생성되었고 기본으로 <code>DATABASE_URL</code> 환경변수를 사용하기 때문에 <code>.env</code> 파일도 생성되었다.</p>
<pre class="line-numbers"><code class="language-clike"># Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
</code></pre>
<p>난 <code>.env</code>는 사용하지 않고 있고 환경변수만 제공하면 되는 것이므로 <code>.env</code> 파일은 삭제하고 따로 환경변수를 제공했다.<br />
<br></p>
<h2>Prisma schema</h2>
<p>앞의 과정에서 생성된 <code>prisma/schema.prisma</code> 파일은 다음의 내용이 포함되어 있다. 이 파일은 Prisma Schema Language(PSL)로 작성된다고 한다. 하지만 PSL에 대한 문서는 못 찾았고 <a href="https://www.prisma.io/docs/concepts/components/prisma-schema#accessing-environment-variables-from-the-schema">스키마 문서</a>에서 문법의 사용 방법을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-clike">// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
</code></pre>
<p><a href="https://www.prisma.io/docs/concepts/components/prisma-schema/generators"><code>generator</code></a>는 스키마에 기반해서 Prisma 클라이언트를 만들어 준다. 여기서는 postgresql을 데이터베이스 프로바이더로 지정했으므로 당연히 여기에 맞는 클라이언트를 만들어 줄 것이고 실제 데이터를 다룰 때는 이 클라이언트를 사용한다. <a href="https://www.prisma.io/docs/concepts/components/prisma-schema/generators#community-generators">문서에 따르면</a> 데이터베이스 스키마를 이용하는 다른 제너레이터도 이용할 수 있다고 한다.</p>
<p><a href="https://www.prisma.io/docs/concepts/components/prisma-schema/data-sources"><code>datasource</code></a>는 Prisma가 데이터베이스에 어떻게 연결하는지를 다룬다. 데이터베이스 URL은 시크릿 정보가 많으므로 <code>env()</code>라는 함수를 제공해 주어 환경변수(여기서는 <code>DATABASE_URL</code>)에서 읽어올 수 있다. 스키마는 단 하나의 데이터소스만 가질 수 있다.<br />
<br></p>
<h2>스키마에 모델 정의하기</h2>
<p>방금 살펴본 <code>prisma/schema.prisma</code>에 데이터베이스 모델을 정의할 수 있다.</p>
<pre class="line-numbers"><code class="language-clike">generator client {} // 생략
datasource db {} // 생략
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
</code></pre>
<p>사용자 테이블을 추가한다고 하면 위처럼 추가할 수 있다. 데이터베이스를 사용해 봤다면 문법은 모르더라도 대충 이해는 할 수 있다. 각 데이터베이스에서 사용할 수 있는 타입과 타입 속성은 <a href="https://www.prisma.io/docs/concepts/database-connectors">데이터베이스 커넥터</a>에서 볼 수 있고 각 데이터베이스의 특성이 있기 때문에 해당 문서를 잘 살펴봐야 한다.</p>
<p>모델을 정의했으니 이를 데이터베이스에 적용해야 하는데 먼저 데이터베이스를 준비하자.</p>
<pre class="line-numbers"><code class="language-bash">$ docker run --name postgres -p 5432:5432 \
-e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres \
postgres
</code></pre>
<p>로컬에 데이터베이스를 설치하는 건 번거롭기 때문에 Docker로 PostgreSQL을 띄웠다. 로컬 PostgreSQL의 설정대로 <code>DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"</code> 환경변수를 지정했다.</p>
<p>정의한 모델을 데이터베이스에 적용할 때는 <a href="https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-dev"><code>prisma migrate dev</code> 명령어</a>를 사용한다. <code>--name</code> 플래그는 이번 마이그레이션의 이름으로 기록을 봤을 때 이해할 수 있는 이름을 지정하면 된다. <code>prisma migrate dev</code> 명령어는 개발하면서 개발환경에서 데이터베이스를 마이그레이션 하는 용도이고 프로덕션은 다른 과정이 필요한 것 같은데 이건 따로 더 살펴볼 예정이다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/prisma migrate dev --name init
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
Applying migration `20220815094441_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20220815094441_init/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
added 2 packages, and audited 24 packages in 2s
found 0 vulnerabilities
✔ Generated Prisma Client (4.2.1 | library) to ./node_modules/@prisma/client in 46ms
</code></pre>
<p>위 결과에 나온 대로 <code>prisma/migrations</code> 폴더 아래 마이그레이션 상황을 기록한 파일이 생기고 폴더 이름에 마이그레이션 한 시간과 앞에서 지정한 <code>init</code>이라는 이름이 나와 있는걸 볼 수 있다.(<code>Generated Prisma Client</code> 부분도 있는데 이건 뒤에서 다시 얘기하겠다.)</p>
<pre class="line-numbers"><code class="language-bash">prisma
├── migrations
│ ├── 20220815094441_init
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
</code></pre>
<p><code>migration.sql</code>을 열어보면 마이그레이션을 위해 사용한 SQL 문을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-sql">-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
</code></pre>
<p>PostgreSQL에 접속해서 확인해 보면 당연히 <code>User</code> 테이블이 생성되었고 <code>_prisma_migrations</code>라는 마이그레이션 상황을 기록하는 테이블도 추가된 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------------+----------+----------
public | User | table | postgres
public | User_id_seq | sequence | postgres
public | _prisma_migrations | table | postgres
(3 rows)
postgres=# SELECT * FROM _prisma_migrations;
id | checksum | finished_at| migration_name | logs | rolled_back_at | started_at | applied_steps_count
--------------------------------------+------------------------------------------------------------------+-------------------------------+---------------------+------+----------------+-------------------------------+---------------------
d12910d5-1d0e-46af-9b82-cc61c9823cac | 74321ef4af21dd277012df04272490dac49483b673162e05f491e8db020c4494 | 2022-08-15 09:44:41.071292+00 | 20220815094441_init | | | 2022-08-15 09:44:41.058691+00 | 1
(1 row)
</code></pre>
<p>마이그레이션 상태는 <a href="https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-status"><code>prisma migrate status</code> 명령어</a>로도 확인해 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">./node_modules/.bin/prisma migrate status
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"
1 migration found in prisma/migrations
Database schema is up to date!
</code></pre>
<p><br></p>
<h2>데이터베이스 사용</h2>
<p>이제 테이블도 준비되었으니 데이터를 추가하기 위해 다음과 같은 <code>add-user.ts</code> 파일을 만들어 보자.</p>
<pre class="line-numbers"><code class="language-typescript">// add-user.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function add() {
const user = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@prisma.io',
},
})
console.log(user)
}
add()
.then(() => {
console.log('added')
})
.catch((err) => {
console.error(err)
process.exit(1)
})
</code></pre>
<p><code>@prisma/client</code>에서 <code>PrismaClient</code> 클라이언트를 임포트했다. 이 부분에서 "응?"하는 분도 있을 수 있는데 앞에서 <code>prisma</code>는 <code>npm install</code>로 설치했지만 <code>@prisma/client</code>를 따로 설치한 적은 없는데 여기서 임포트를 했고 <code>npm ls</code>로도 <code>@prisma/client</code>가 설치된 것으로 나온다.</p>
<pre class="line-numbers"><code class="language-bash">$ npm ls
prisma-demo@1.0.0 /Users/outsider/prisma-demo
├── @prisma/client@4.2.1
├── @types/node@18.7.3
├── prisma@4.2.1
├── ts-node@10.9.1
└── typescript@4.7.4
</code></pre>
<p>앞에서 <code>prisma migrate dev</code>를 실행했을 때 <code>Generated Prisma Client (4.2.1 | library) to ./node_modules/@prisma/client in 46ms</code>라는 부분이 출력되었다. 이 클라이언트는 <code>npm install</code>로 추가되는 게 아니라 실제로 내가 정의한 데이터베이스 스키마에 따라(여기서는 User 모델을 가진) 생성되는 클라이언트이기 때문에 마이그레이션 할 때 자동으로 생성되고 마치 설치한 모듈처럼 <code>node_modules</code> 밑에 추가된다. 그래서 위에서 설치는 안 했지만 임포트해서 사용할 수 있는 것이다. 이는 스키마를 변경하면 클라이언트도 새로 생성해야 한다는 의미가 되고 보통 <code>node_modules</code>는 Git에 추가하지 않기 때문에 배포나 패키징을 할 때 클라이언트도 생성되도록 관리가 필요하다는 의미가 된다.</p>
<p>클라이언트만 생성하고 싶다면 <a href="https://www.prisma.io/docs/reference/api-reference/command-reference#generate"><code>prisma generate</code> 명령어</a>를 사용하면 된다.</p>
<p>이 클라이언트가 스키마를 이해하고 있기 때문에 <code>prisma.user.create()</code>처럼 사용할 수 있다. 추가적인 쿼리는 <a href="https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#model-queries">모델 쿼리 문서</a>에서 찾을 수 있다.</p>
<p>이 TypeScript 코드를 실행하면 사용자가 잘 추가되는 것을 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ ./node_modules/.bin/ts-node add-user.ts
{ id: 1, email: 'alice@prisma.io', name: 'Alice' }
added
</code></pre>
<p><strong><a href="https://blog.outsider.ne.kr/1614?commentInput=true#entry1614WriteComment">댓글 쓰기</a></strong></p>GitHub Actions로 npm publish 자동화하기Outsiderhttps://blog.outsider.ne.kr/15592021-08-13T23:39:57+09:002021-08-13T23:39:57+09:00<p>npm 모듈을 배포해서 사용하는 경우 예전에는 <a href="https://www.npmjs.com/">npm 레지스트리</a>를 사용했지만 이젠 <a href="https://github.com/features/packages">GitHub Packages</a>가 있으므로 GitHub 저장소를 이용하고 Actions를 쓴다면 <a href="https://github.com/features/packages">GitHub Packages</a>도 대안이 될 수 있다. 특히 공개적으로 쓰는 모듈이 아니라 내부에서만 쓴다면 npm을 결제해서 쓸 필요 없이 바로 <a href="https://github.com/features/packages">GitHub Packages</a>를 사용하는 게 더 편하다.(회사 자체는 npm도 GitHub이긴 하지만...) 이 글에 의도가 꼭 GitHub Packages를 써야 하는 건 아니지만 여기서는 GitHub Packages를 사용했다.</p>
<p>이런 모듈의 경우 레지스트리의 배포를 해야 하므로 로컬에서 매번 하는 대신 GitHub Actions에서 자동으로 발행되게 하면 더 편하게 배포를 할 수 있다. 오픈소스의 경우 npm을 이용할 때 몇 번의 사고로 2FA를 이용하고 있어서 자동화하기 어려운 면이 있는데(요즘 공개적으로 모듈을 배포할 일이 없어서 따로 연구는 안 해봤다.) 회사 등에서 쓰는 경우에는 어차피(?) 다 GitHub이므로 이런 부분까지 더 자동화할 수 있는 것 같다.(보안을 생각 안 해도 된다기보다 제어할 수 있는 부분이 훨씬 많고 영향 범위도 적기 때문에...)<br />
<br></p>
<h1>GitHub Actions로 npm 모듈 Packages에 발행하기</h1>
<p><a href="https://docs.github.com/en/actions/guides/publishing-nodejs-packages">GitHub 문서</a>에 npm 레지스트리에 발행하기와 GitHub Packages에 발행하는 방법이 잘 나와 있지만 일단 <code>publish.yaml</code>을 만들어 보자.</p>
<pre class="line-numbers"><code class="language-yaml">name: Publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v2
# Setup .npmrc file to publish to GitHub Packages
- uses: actions/setup-node@v2
with:
node-version: '16.x'
registry-url: 'https://npm.pkg.github.com'
- run: npm install
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
</code></pre>
<p>약간 설명을 하자면 <code>on</code> 에서 <code>v</code>로 시작하는 태그가 푸시될 때 워크플로우가 실행되도록 했다. 배포할 때는 보통 버전을 붙이니까 태그를 이용해서 배포되도록 한 것이고 이렇게 하면 실행된 태그를 활용해서 버저닝 등에도 활용할 수 있다.</p>
<p>그리고 <a href="https://github.com/actions/setup-node">actions/setup-node</a>이 <code>.npmrc</code> 파일을 설정해 주므로 공식 레지스트리가 아니라 GitHub Packages에 배포할 수 있게 설정해 준다. 워크플로우에서 시크릿에 <code>GITHUB_TOKEN</code>을 자동으로 설정해 주므로 <code>permissions</code>를 이용해서 이 토큰의 <a href="https://docs.github.com/en/actions/reference/authentication-in-a-workflow">권한을 추가로 설정</a>해서 <code>packages</code>에 쓰기 권한을 줬다.</p>
<p>GitHub Packages를 쓰는 경우 사용자가 org 아래 배포되므로 <a href="https://docs.npmjs.com/cli/v7/using-npm/scope">npm의 scope</a>를 <code>package.json</code>의 <code>name</code>에 <code>@username/module-name</code> 형식으로 지정해 주어야 한다. 추가로 Packages를 저장소와 연결해서 권한을 확인하기 때문인지 <code>repository</code> 필드도 저장소를 가리키도록 설정해야 한다. 이 설정이 없으면 <code>npm publish</code> 할 때 404 오류가 발생한다. <code>404 Not Found - PUT https://npm.pkg.github.com/@outsideris%2fexample-module - The expected resource was not found.</code>, <code>'@outsideris/example-module@0.3.3' is not in the npm registry</code>의 오류가 발생한다.</p>
<pre class="line-numbers"><code class="language-json">{
"name": "@outsideris/example-module",
"repository": {
"type": "git",
"url": "https://github.com/outsideris/example-module.git"
}
}
</code></pre>
<p>계정 밑의 Packages 메뉴에 가면 아래처럼 잘 배포된 것을 볼 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/1770582221.png" width="750" height="271" alt="GitHub Packages에 배포된 npm 모듈" title="" /></p>
<p>이제 릴리스가 필요할 때 <code>package.json</code>에서 버전을 올리고 Git 태그를 붙여서 푸시하면 자동으로 퍼블리싱된다.<br />
<br></p>
<h1>버전 업데이트와 태깅 자동화</h1>
<p>보통 이렇게 사용하지만 여기서 좀 더 편하게 하고 싶었다. Docker 이미지 배포 같은 경우는 이런 워크플로우로 충분하지만 npm 모듈 같은 경우에는 <code>package.json</code>에 버전이 있으므로(<code>package-lock.json</code>에도 있다) 릴리스할 때 이 버전을 반드시 올려주어야 한다. Pull Request를 중심으로 작업하는 경우 번거롭게 버전을 올리는 커밋을 따로 올려야 하는 데다가 버전을 올린 뒤에 Git 태그도 붙여주어야 하지만 Git 태그는 Pull Request로 할 수 없기 때문에 결국 릴리스는 푸시로 하게 되곤 한다. 릴리스 자체는 결국 개발자가 개입해야 하지만 이 부분까지 더 편하게 하고 싶었다.</p>
<p>npm에는 버전을 올려주는 <a href="https://docs.npmjs.com/cli/v7/commands/npm-version"><code>npm version</code></a> 명령어가 있다. <code>npm version patch</code>를 실행하면 패치 버전을 올려준다. 즉, 현재 버전이 <code>0.3.4</code>일 때 실행하면 <code>0.3.5</code>가 되고 상황에 따라 <code>patch</code> 대신 <code>minor</code>나 <code>major</code>를 지정하면 된다.</p>
<pre class="line-numbers"><code class="language-bash">$ npm version patch
v0.3.5
</code></pre>
<p>버전만 올려주는 게 아니라 Git 커밋도 해주고 태그도 붙여준다. 버전을 올린 후 최신 커밋을 확인해 보면 <code>package.json</code>과 <code>package-lock.json</code>에서 버전을 올려준 뒤 <code>0.3.5</code>라는 새 커밋을 만들고 여기에 <code>v0.3.5</code> 태그를 추가한 것을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ git show HEAD
commit 68427365956a55386b51a9cbf5ddee4c6bd46b88 (HEAD -> master, tag: v0.3.5)
Author: Outsider <outsideris@gmail.com>
Date: Fri Aug 13 22:35:49 2021 +0900
0.3.5
diff --git a/package-lock.json b/package-lock.json
index d6719e0..f025c67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@outsideris/example-module",
- "version": "0.3.4",
+ "version": "0.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@outsideris/example-module",
- "version": "0.3.1",
+ "version": "0.3.5",
"dependencies": {
"debug": "^4.3.2"
}
diff --git a/package.json b/package.json
index d6ef099..6520c99 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@outsideris/example-module",
- "version": "0.3.4",
+ "version": "0.3.5",
"repository": {
"type": "git",
"url": "https://github.com/outsideris/example-module.git"
</code></pre>
<p>간단히 말하면 <code>npm version</code> 명령어를 실행한 뒤에 <code>git push</code>만 해주면 앞에서 만든 트리거가 실행된다. 하지만 이때 <code>npm version</code>의 사용법을 개발자가 모두 잘 알고 있어야 하고 여전히 직접 <code>push</code> 해주어야 하는 문제가 있다. 너무 감쪽같이 커밋을 만들어 주기 때문에 이전에 실수로 버전을 잘못 올리고 배포해 버린 적도 있다.</p>
<p><code>bump-version.yaml</code> 파일을 만들어서 새로운 워크플로우를 하나 추가했다.</p>
<pre class="line-numbers"><code class="language-yaml">name: Bump Version
on: workflow_dispatch
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: git config --global user.email "github-actions@example.com"
- run: git config --global user.name "GitHub Actions"
- uses: actions/setup-node@v1
with:
node-version: 16
- run: npm version patch
- run: git push origin master --tags
- uses: actions/upload-artifact@v2
with:
name: src
path: ./
</code></pre>
<p>이 워크플로우는 <a href="https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow"><code>workflow_dispatch</code></a>로 지정했으므로 Action 메뉴에서 수동으로 실행해 주어야 한다. 배포가 필요할 때 직접 누르겠다는 의미다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5123201374.png" width="750" height="366" alt="workflow_dispatch 워크플로우" title="" /></p>
<p>여기서 "Run workflow"를 클릭하면 "Bump Version" 워크플로우가 실행되고 여기서 하는 일은 패치 버전을 올리고 생성된 커밋을 저장소에 다시 커밋한다. 커밋을 하기 위해 임의의 이름으로 Git 사용자 정보를 등록했다. 그리고 이어진 작업을 하기 위해 현재 디렉터리 전체를 아티팩트로 등록했다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5444086943.png" width="750" height="458" alt="등록된 아티팩트" title="" /></p>
<p>즉, 이 워크플로우가 버전을 올리고 릴리스 커밋과 태그를 등록해 준다. 다만 워크플로우에서 올린 푸시는 이벤트를 발생시키지 않으므로 앞에서 만든 <code>publish.yaml</code> 워크플로우가 트리거 되지 않는다. 그래서 <code>publish.yaml</code>을 다음과 같이 <code>workflow_run</code>을 이용하도록 수정했다.</p>
<pre class="line-numbers"><code class="language-yaml">name: Publish
on:
workflow_run:
workflows: ["Bump Version"]
types: [completed]
jobs:
publish:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
actions: read
steps:
- name: 'Download artifact'
uses: actions/github-script@v3.1.0
with:
script: |
var artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{github.event.workflow_run.id }},
});
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "src"
})[0];
var download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
var fs = require('fs');
fs.writeFileSync('${{github.workspace}}/src.zip', Buffer.from(download.data));
- run: unzip src.zip
# 중략
- run: npm publish
</code></pre>
<p>아래 생략한 부분은 앞부분과 동일하므로 <code>npm publish</code>만 남기고 생략했다.</p>
<pre class="line-numbers"><code class="language-yaml">on:
workflow_run:
workflows: ["Bump Version"]
types: [completed]
</code></pre>
<p><code>workflow_run</code>을 지정했으므로 "Bump Version" 워크플로우가 완료되면 이 워크플로우가 실행된다. 성공/실패 여부는 여기서 조건을 걸 수 없으므로 <code>if: ${{ github.event.workflow_run.conclusion == 'success' }}</code>로 성공했을 때만 실행되도록 했다.</p>
<p>"Download artifact" 부분에서 앞에서 올린 아티팩트를 다운받게 했는데 사실상 다른 워크플로우의 아티팩트이므로 이미 만들어진 액션 대신 API로 조회해서 직접 아티팩트를 찾아서 다운받게 했고 이 코드는 <a href="https://securitylab.github.com/research/github-actions-preventing-pwn-requests/">GitHub의 글</a>에서 참고해서 가져왔다. 권한 오류를 해결하기 위해 <code>permissions</code>에 <code>actions: read</code>를 추가했다.(없으면 API 오류가 발생한다.) 체크아웃하지 않고 아티팩트를 받아온 이유는 앞에서 실행한 커밋의 소스를 그대로 이용하기 위함이다. 사실상 별도의 워크플로우이기 때문에 다시 실행하면 다른 커밋이 들어올 수도 있을 것 같다.</p>
<p>이제 배포가 필요할 때 "Bump Version"을 실행하면 버전업 커밋이 생기고 "Publish" 워크플로우가 실행되면서 GitHub Packages에 새 버전의 모듈이 배포된다.</p>
<p>이렇게 설정은 처음 해봤는데 배포에 실수할 여지가 줄어들어서 괜찮아 보인다. patch 버전만 올릴 수 있지만, input으로 입력 받거나(셀렉트 박스는 안되어서 실수의 여지가 있다.) Bump Version 워크플로우를 종류별로 만들어도 될 것 같다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1559?commentInput=true#entry1559WriteComment">댓글 쓰기</a></strong></p>Mocha v8.0.0의 병렬 테스트Outsiderhttps://blog.outsider.ne.kr/14892020-07-24T20:05:02+09:002020-07-24T20:05:00+09:00<h2>Mocha v8.0.0의 병렬 테스트</h2>
<p>이 글은 <a href="https://developer.ibm.com/">IBM 개발자 사이트</a>에 Mocha의 리드 메인테이너인 <a href="https://github.com/boneskull">Christopher Hiller</a>가 Mocha V8에 추가된 병렬 테스트 모드에 대해서 소개한 글을 번역한 글입니다. 번역은 IBM과 Christopher Hiller의 허락을 받아 진행했습니다.</p>
<hr />
<p>Mocha는 <a href="https://github.com/mochajs/mocha/releases/tag/v8.0.0">v8.0.0 릴리스</a>에서 Node.js에서 병렬 모드를 지원하기 시작했다. Mocha로 병렬 모드로 테스트를 실행하면 멀티코어 CPU의 이점을 얻어서 대규모 테스트 스위트에서 속도를 크게 향상할 수 있다.</p>
<p><a href="https://mochajs.org/#parallel-tests">Mocha 문서</a>의 병렬 테스트 부분을 읽어보길 바란다.</p>
<p>v8.0.0 이전까지 Mocha는 직렬로만 테스트를 실행했다. 다음 테스트로 넘어가기 전에 테스트가 반드시 종료되어야 한다. 적은 수의 테스트 스위트에서는 결정적이고 빠르다는 장점은 있지만, 대량의 테스트를 실행할 때는 병목이 될 수 있다.</p>
<p>실제 프로젝트에서 어떻게 Mocha의 병렬 모드를 사용해서 장점을 얻을 수 있는지 살펴보자.</p>
<h1>설치</h1>
<p>Node.js v8.0.0의 생명주기가 끝났으므로 Mocha v8.0.0는 Node.js v10, v12, v14가 필요하다.</p>
<p>Mocha 자체를 꼭 설치할 필요는 없지만, 원하면 해도 된다. Mocha v8.0.0 이상의 버전이 필요한데 다음과 같이 설치할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">npm i mocha@8 --save-dev
</code></pre>
<p><br></p>
<h1><code>--parallel</code> 플래그의 사용</h1>
<p>많은 경우 <code>mocha</code>를 실행할 때 <code>--parallel</code> 지정하면 병렬 모드를 활성화 할 수 있다.</p>
<pre class="line-numbers"><code class="language-clike">mocha --parallel test/*.spec.js
</code></pre>
<p>아니면 Mocha의 <a href="https://mochajs.org/#configuring-mocha-nodejs">설정 파일</a>을 사용해서 명령행 플래그를 지정할 수도 있다. Mocha는 기본 설정인 <code>.mocharc.yml</code> YAML 파일을 가지고 있고 다음과 같이 생겼다.(간결하게 일부는 제외했다.)</p>
<pre class="line-numbers"><code class="language-yaml"># .mocharc.yml
require: 'test/setup'
ui: 'bdd'
timeout: 300
</code></pre>
<p>병렬 모드를 활성화하려고 이 파일에 <code>parallel: true</code>를 추가할 것이다.</p>
<pre class="line-numbers"><code class="language-yaml"># .mocharc.yml w/ parallel mode enabled
require: 'test/setup'
ui: 'bdd'
timeout: 300
parallel: true
</code></pre>
<p>이후 예제에서는 명확하게 <code>--parallel</code>나 <code>--no-parallel</code>를 사용한다.</p>
<p><code>npm test</code>를 실행하고 무슨 일이 벌어지는지 보자!</p>
<h1>스포일러: 처음엔 동작하지 않는다</h1>
<p>앗! 유닛테스트에서 기본 timeout 값(위에 나왔던 300ms)을 사용한 "timeout" 예외가 다수 발생했다.</p>
<pre class="line-numbers"><code class="language-bash"> 2) Mocha
"before each" hook for "should return the Mocha instance":
Error: Timeout of 300ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/boneskull/projects/mochajs/mocha/test/node-unit/mocha.spec.js)
at Hook.Runnable._timeoutError (lib/runnable.js:425:10)
at done (lib/runnable.js:299:18)
at callFn (lib/runnable.js:380:7)
at Hook.Runnable.run (lib/runnable.js:345:5)
at next (lib/runner.js:475:10)
at Immediate._onImmediate (lib/runner.js:520:5)
at processImmediate (internal/timers.js:456:21)
</code></pre>
<p>이상한 결과가 나왔다. 다시 테스트를 실행하자 다른 테스트가 "timeout" 예외를 던졌다. 왜 그런가?</p>
<p>Mocha에서부터 Node.js, OS, CPU에 걸친 다양한 변수로 인해 병렬 모드는 주어진 테스트에 대해 훨씬 더 넓은 범위의 타이밍을 보여준다. 이러한 timeout 예외는 새로운 성능 이슈를 보여주는 것이 아니라 자연스럽게 더 높아진 시스템 부하와 비결정적 실행 순서의 증상이다.</p>
<p>이를 해결하기 위해 기본 테스트 타임 만료 시간을 300ms(0.3초)에서 1,000ms(1초)로 올렸다.</p>
<pre class="line-numbers"><code class="language-yaml"># .mocharc.yml
# ...
timeout: 1000
</code></pre>
<p>Mocha의 "timeout" 기능은 벤치마킹용이 아니라 의도치 않게 실행에 오래 걸리는 코드를 잡아내기 위해 만들어진 것이다. 테스트 실행이 더 오래 걸릴 수 있음으로 안심하고 <code>timeout</code> 값을 증가시킬 수 있다.</p>
<p>이제 테스트가 성공했으므로 더 많은 테스트를 성공시키려고 한다.</p>
<h1>병렬 모드 최적화</h1>
<p>기본적으로 Mocha의 최대 잡 개수는 n-1이고 여기서 n은 기기의 CPU 코어 수이다. 이 기본값이 모든 프로젝트의 최적값은 아닐 것이다. 잡 개수도 운영체제에 따라 다르므로 "Mocha가 n-1 CPU 코어를 사용한다"를 의미하지는 않는다. 하지만, 이 값이 기본값이고 보통 기본값이 하는 일을 한다.</p>
<p>"최대 잡 개수"를 언급할 때 이는 Mocha가 필요하다면 이 개수만큼의 워커 프로세스를 생성할 수 있고 이는 테스트 파일의 수와 실행 시간에 따라 다르다는 의미이다.</p>
<p>성능을 비교해 보기 위해 익숙한 벤치마크 도구인 <a href="https://github.com/sharkdp/hyperfine">hyperfine</a>을 사용할 것이다. 이를 통해 다양한 설정에 어떻게 동작하는지 알 수 있을 것이다.</p>
<p><code>hyperfine</code> 사용법에 따라 아래 예제에서 <code>hyperfine</code>에 두 가지 옵션을 전달하고 있다. <code>-r 5</code>는 명령어를 5번 실행하라는 의미이고 기본값은 10이지만 이는 기다리기에 너무 느리다. 지정한 두 번째 옵션은 <code>--warmup 1</code>인데 이는 한 번의 "warmup" 실행을 뜻한다. 이 실행 결과는 버려진다.</p>
<p>warmup 실행은 첫 k번의 실행이 이어진 실행보다 확연히 느려서 최종 결과를 왜곡하는 것을 줄여준다. 이런 일이 벌어지면 <code>hyperfine</code>이 이에 관해 경고해 줄 것이다. 그래서 여기서 이 옵션을 사용하는 것이다!</p>
<p>직접 실행해 보려면 환경에 따라 <code>bin/mocha</code>를 <code>node_modules/.bin/mocha</code>나 <code>mocha</code>로 바꿔야 한다. <code>bin/mocha</code>는 작업 폴더에서 <code>mocha</code> 실행 파일의 상대 경로이다.</p>
<p>Mocha의 통합 테스트(약 55개 파일의 260 테스트)는 보통 <code>mocha</code> 실행 파일의 출력을 assertion 한다. 유닛테스트보다 더 긴 <code>timeout</code> 값이 필요하고 아래에서 10초의 timeout을 사용한다.</p>
<p>직렬 모드로 통합 테스트를 실행했다. 말도 안 되는 속도에 아무도 불만을 얘기하지 않았다.</p>
<pre class="line-numbers"><code class="language-bash">$ hyperfine -r 5 --warmup 1 "bin/mocha --no-parallel --timeout 10s test/integration/**/*.spec.js"
Benchmark #1: bin/mocha --no-parallel --timeout 10s test/integration/**/*.spec.js
Time (mean ± σ): 141.873 s ± 0.315 s [User: 72.444 s, System: 14.836 s]
Range (min … max): 141.447 s … 142.296 s 5 runs
</code></pre>
<p>2분 이상이 걸렸다. 이를 병렬 모드로 실행해 보자. 내 환경에서는 8코어 CPU(<code>n = 8</code>)를 쓰고 있음으로 Mocha는 기본적으로 7개의 워커 프로세스를 사용한다.</p>
<pre class="line-numbers"><code class="language-bash">$ hyperfine -r 5 --warmup 1 "bin/mocha --parallel --timeout 10s test/integration/**/*.spec.js"
Benchmark #1: bin/mocha --parallel --timeout 10s test/integration/**/*.spec.js
Time (mean ± σ): 65.235 s ± 0.191 s [User: 78.302 s, System: 16.523 s]
Range (min … max): 65.002 s … 65.450 s 5 runs
</code></pre>
<p>병렬 모드를 사용해서 1분 이상인 76초를 줄였다. 거의 53%의 속도 향상이 있었다. 여기서 다 향상할 수 있는가?</p>
<p>Mocha가 얼마나 많은 워커 프로세스를 사용할지 정확히 지정하려고 <code>--jobs</code>/<code>-j</code> 옵션을 사용할 수 있다. 이 숫자를 4로 줄이면 무슨 일이 벌어지는지 살펴보자.</p>
<pre class="line-numbers"><code class="language-clike">$ hyperfine -r 5 --warmup 1 "bin/mocha --parallel --jobs 4 --timeout 10s test/integration/**/*.spec.js"
Benchmark #1: bin/mocha --parallel --jobs 4 --timeout 10s test/integration/**/*.spec.js
Time (mean ± σ): 69.764 s ± 0.512 s [User: 79.176 s, System: 16.774 s]
Range (min … max): 69.290 s … 70.597 s 5 runs
</code></pre>
<p>안타깝게도 더 느려졌다. 대신 이 숫자를 늘리면 어떻게 되는지 살펴보자.</p>
<pre class="line-numbers"><code class="language-clike">$ hyperfine -r 5 --warmup 1 "bin/mocha --parallel --jobs 12 --timeout 10s test/integration/**/*.spec.js"
Benchmark #1: bin/mocha --parallel --jobs 12 --timeout 10s test/integration/**/*.spec.js
Time (mean ± σ): 64.175 s ± 0.248 s [User: 80.611 s, System: 17.109 s]
Range (min … max): 63.809 s … 64.400 s 5 runs
</code></pre>
<p>기본값인 7보다는 12로 지정했을 때 약간 빨라졌다. 테스트 환경이 8코어임을 생각해 봐라. 왜 더 많은 프로세스를 만드니까 성능이 향상되었을까?</p>
<p>이는 테스트가 CPU에 의존하지 않기 때문으로 추측하고 있다. 테스트는 대부분 비동기 I/O를 수행하고 있음으로 CPU는 테스트를 완료되기를 기다리는 여유 사이클을 가지게 된다. 이 테스트의 500ms를 더 줄이려고 시간을 들일 수 있지만, 이 글의 목적은 아니다. 완벽함은 좋음의 적이지 않은가? 핵심은 각자의 프로젝트에 이 전력을 어떻게 적용하고 만족할 설정이 무엇인지 보여주는 것이다.</p>
<h1>언제 병렬 모드를 피해야 하는가?</h1>
<p>테스트를 항상 병렬로 돌리는 게 적절치 않는다고 말한다면 놀랄 것인가? 병렬 모드가 항상 좋은 것은 아니다. 두 가지를 이해해야 한다.</p>
<ol>
<li><strong>Mocha는 개별 테스트를 병렬로 실행하지 않는다.</strong> Mocha는 테스트 파일을 병렬로 실행한다.</li>
<li>워커 프로세스를 생성하는 것은 공짜가 아니다.</li>
</ol>
<p>즉 하나의 Mocha 테스트 파일이 있다면 하나의 워커 프로세스를 생성하고 이 프로세스가 파일을 실행할 것이다. 테스트 파일이 딱 하나만 있다면 병렬 모드에서 불이익을 받게 됩니다. 이렇게 하면 안 된다.</p>
<p>"하나의 파일"을 사용하는 일반적이지 않은 경우 외에도 테스트나 소스의 고유한 특정이 결과에 영향을 미칠 것입니다. 직렬로 테스트를 실행하는 것보다 병렬로 실행하는 것이 느린 변곡점이 있다.</p>
<p>실제로 Mocha 자체의 테스트(약 35개 파일의 740개 테스트)가 좋은 예시입니다. 좋은 유닛 테스트와 마찬가지로 I/O 없이 독립적으로 빠르게 실행하려고 한다. 기준선을 위해 직렬로 Mocha의 유닛테스트를 실행했다.</p>
<pre class="line-numbers"><code class="language-clike">$ hyperfine -r 5 --warmup 1 "bin/mocha --no-parallel test/*unit/**/*.spec.js"
Benchmark #1: bin/mocha --no-parallel test/*unit/**/*.spec.js
Time (mean ± σ): 1.262 s ± 0.026 s [User: 1.286 s, System: 0.145 s]
Range (min … max): 1.239 s … 1.297 s 5 runs
</code></pre>
<p>이제 병렬로 실행해 보자. 내 기대와 달리 결과는 다음과 같이 나왔다.</p>
<pre class="line-numbers"><code class="language-clike">$ hyperfine -r 5 --warmup 1 "bin/mocha --parallel test/*unit/**/*.spec.js"
Benchmark #1: bin/mocha --parallel test/*unit/**/*.spec.js
Time (mean ± σ): 1.718 s ± 0.023 s [User: 3.443 s, System: 0.619 s]
Range (min … max): 1.686 s … 1.747 s 5 runs
</code></pre>
<p>객관적으로 Mocha의 유닛 테스트를 병렬로 실행하는 것이 약 0.5초 느리다. 이는 워커 프로세스를 생성하는 부하이다.(그리고 프로세스 간의 통신을 위한 직렬화도 필요하다.)</p>
<p>매우 빠른 유닛 테스트를 가진 다수의 프로젝트는 Mocha의 병렬 모드의 혜택을 받지 못할 것으로 예상한다.</p>
<p>앞에서 작성한 <code>.mocharc.yml</code>를 기억하는가? 설정 파일에서 <code>parallel: true</code>를 제거했다. 대신 Mocha는 Mocha의 통합 테스트를 실행할 때만 병렬 모드를 사용한다.</p>
<p>보통 이러한 형식의 테스트와 맞지 않는 것 외에도 병렬 모드는 다른 제약사항이 있다. 아래에서 이에 관해 얘기하겠다.</p>
<h1>주의 사항, 고지 사항, 발견한 내용</h1>
<p>기술적인 제약(예: "이유")때문에 몇몇 기능은 병렬 모드와 호환되지 않는다. 이를 사용하면 Mocha가 예외를 던질 것이다.</p>
<p>추가적인 정보와 (있다면)우회방법은 <a href="https://mochajs.org/#parallel-tests">문서를 확인</a>해 봐라.</p>
<h2>지원하지 않는 보고서</h2>
<p><code>markdown</code>, <code>progress</code>, <code>json-stream</code> 보고서를 사용하고 있다면 병렬 모드에서는 사용할 수 없다. 이 보고서들은 얼마나 많은 테스트를 실행할지 미리 알아야 한다. 병렬 모드는 이러한 정보를 가지고 있지 않다.</p>
<p>나중에는 달라질 수 있지만 이러한 보고서는 하위호환이 안 되는 변경을 하게 될 것이다.</p>
<h2>단독 테스트</h2>
<p>단독 테스트(<code>.only()</code>)는 동작하지 않는다. 단독 테스트를 사용하면 Mocha는 <code>.only()</code>를 사용한 곳까지는 테스트를(<code>.only()</code>를 사용하지 않았으므로)를 실행하고 <code>.only()</code>를 만나면 중단하고 실패할 것이다.</p>
<p>단독 테스트는 보통 하나의 파일에서 하나의 파일에서 사용되므로 병렬 모드는 이 상황에 적합하지 않다.</p>
<h2>지원하지 않는 옵션</h2>
<p><code>--sort</code>, <code>--delay</code> 옵션 특히 <code>--file</code> 옵션은 호환되지 않는다. 간단히 말하면 테스트를 지정한 순서로 실행할 수 없기 때문이다.</p>
<p>이 옵션 중 <code>--file</code>이 아마 가장 많은 수의 프로젝트에 영향을 줄 것이다. Mocha v8.0.0 이전에는 "루트 훅"을 정의할 때 <code>--file</code>을 사용하도록 추천했다. 루트 훅(<code>beforeEach()</code>, <code>after()</code>, <code>setup()</code> 등과 같은)은 다른 모든 파일이 상속받는다. 이 접근은 루트 훅을 이 파일에 정의하는 것으로 예를 들어 <code>hooks.js</code>로 Mocha를 다음과 같이 실행한다.</p>
<pre class="line-numbers"><code class="language-clike">mocha --file hooks.js "test/**/*.spec.js"
</code></pre>
<p>모든 <code>--file</code> 파라미터는 테스트 파일로 간주하여 다른 테스트 파일(이 경우에는 <code>test/**/*.spec.js</code>)보다 먼저 실행될 것이다. 이를 보장해주므로 Mocha는 <code>hooks.js</code>에 정의된 훅으로 "부트스트랩"을 수행하고 이는 이어진 모든 테스트 파일에 영향을 준다.</p>
<p>이는 Mocha v8.0.0에서도 여전히 동작하지만, 직렬 모드에서만 동작한다. 하지만! 강력히 사용하지 말기를 권한다.(결국은 완전히 폐기될 것이다.) 대신 Mocha는 Root Hook 플러그인을 도입했다.</p>
<h1>루트 훅 플러그인</h1>
<p>루트 훅 플러그인은 <code>mochaHooks</code>라는 이름으로 익스포트 된 모듈(CJS나 ESM)로 이 모듈에서 사용자가 자유롭게 훅을 정의할 수 있다. 루트 훅 플러그인 모듈은 Mocha의 <code>--require</code> 옵션으로 로드된다.</p>
<p><a href="https://mochajs.org/#root-hook-plugins">루트 훅 플러그인 사용에 관한 문서를 읽어봐라</a>.</p>
<p>위에 링크한 문서에 자세한 설명과 예시가 있지만 여기서 간단히 설명하겠다.</p>
<p><code>--file hooks.js</code>로 로드되는 루트 훅을 가진 프로젝트가 있다고 해보자.</p>
<pre class="line-numbers"><code class="language-clike">// hooks.js
beforeEach(function() {
// do something before every test
this.timeout(5000); // trivial example
});
</code></pre>
<p>이를 루트 훅 플러그인으로 바꾸려면 <code>hooks.js</code>를 다음과 같이 바꾼다.</p>
<pre class="line-numbers"><code class="language-clike">// hooks.js
exports.mochaHooks = {
beforeEach() {
this.timeout(5000);
}
};
</code></pre>
<p>팁: 이를 ESM 모듈로 작성할 수도 있다. 예를 들면 <code>hooks.mjs</code>로 만들어서 네임드 익스포트 <code>mochaHooks</code>를 사용하면 된다.</p>
<p><code>mocha</code>를 실행할 때 <code>--file hooks.js</code>를 <code>--require hooks.js</code>로 바꾸면 된다. 간단하다!</p>
<h1>병렬 모드 트러블 슈팅</h1>
<p>많은 프로젝트에서 병렬 모드가 잘 동작하지만, 문제를 겪고 있다면 테스트를 준비하면서 다음 체크리스트를 참고해라.</p>
<ul>
<li>✅ <a href="https://mochajs.org/#reporter-limitations">지원하는 보고서</a>를 사용하는지 확인.</li>
<li>✅ <a href="https://mochajs.org/#file-order-is-non-deterministic">지원하지 않는 플래그</a>를 사용하지 않는지 확인.</li>
<li>✅ <a href="https://mochajs.org/#configuring-mocha-nodejs">설정 파일</a>을 다시 확인. 설정 파일에 설정된 옵션은 다른 커맨드라인 옵션과 합쳐진다.</li>
<li>✅ 테스트에서 루트 훅을 찾는다.(<a href="https://mochajs.org/#root-hooks-are-not-global">문서</a>에 나온 형태이다.) 이를 <a href="https://mochajs.org/#root-hook-plugins">루트 훅 플러그인</a>으로 바꾼다.</li>
<li>✅ 사용하는 assertion, mock, 그 외 테스트 라이브러리에서 루트 훅을 사용하는가? 병렬 모드와 호환되려면 <a href="https://mochajs.org/#migrating-a-library-to-use-root-hook-plugins">마이그레이션</a>을 해야 한다.</li>
<li>✅ 테스트가 예상과 달리 타임 만료가 된다면 기본 테스트 타임 만료 시간을 늘려야 할 것이다. (<a href="https://mochajs.org/#-timeout-ms-t-ms"><code>--timeout</code></a> 사용)</li>
<li>✅ 테스트가 특정 순서로 실행되지 않아도 괜찮은지 확인</li>
<li>✅ 테스트가 스스로 정리하도록 해라. 임시 파일, 핸들, 소켓 등 삭제하고 테스트 파일 간에 상태나 리소스를 공유하면 안 된다.</li>
</ul>
<h1>다음은?</h1>
<p>병렬 모드는 새 기능이고 아직 완전하지 않다. 개선할 부분이 많이 있지만 이를 위해서 Mocha는 여러분의 도움이 필요합니다. <a href="https://github.com/mochajs/mocha/issues">피드백</a>을 Mocha 팀에 보내주세요. <a href="https://github.com/mochajs/mocha/releases/tag/v8.0.0">Mocha v8.0.0</a>을 사용해서 병렬 모드를 활성화하고 루트 훅 플러그인을 사용해 본 뒤 의견을 공유해 주세요.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1489?commentInput=true#entry1489WriteComment">댓글 쓰기</a></strong></p>