본문으로 건너뛰기

에러로그: deno deploy에서는 Worker API를 지원하지 않습니다.

· 약 25분
arch-spatula

tl;dr : deno deploy 런타임에서는 Worker를 사용할 수 없습니다. 하지만 Bcrypt은 Worker를 사용하고 있습니다. Bcrypt 라이브러리를 Deno deploy에서 사용하면서 발생한 에러입니다.

문제: 개발환경에서 요청을 보낼 때랑 배포환경에서 요청을 보낼 때 동작이 다릅니다.

요청: POST, /api/auth/signup

{
"email": "username@email.com",
"password": "12345678"
}

응답:

{
"success": false,
"msg": "Error: 이미 가입한 아이디입니다."
}

요청: POST, /api/auth/signin

{
"email": "username@email.com",
"password": "12345678"
}

응답:

{
"success": false,
"msg": "Error: 이메일이 없습니다."
}

시도: Docker가 배우고 싶어졌습니다.

가설 설정도 하기 어려웠습니다.

ChatGPT 선생님에게 일단 질문했습니다.

Deno에서 개발환경에서 oak로 요청을 보낼 때랑 Deno deploy로 배포하고 요청을 보냈을 때 동작이 다릅니다.

(위 내용을 넣었습니다.)

참고로 ENV 환경 변수 설정은 올바르게 되었습니다.

(질문용 코드는 생략하겠습니다.)

얻은 답변입니다.

주어진 코드를 통해 Deno 개발환경과 Deno Deploy 환경에서의 동작 차이를 파악할 수 있습니다. 문제의 원인은 Deno Deploy 환경에서 MongoDB와의 통신이 예상대로 작동하지 않는 것입니다.

Deno Deploy는 클라우드 환경에서 실행되며, 이 경우에는 로컬 개발환경과는 다른 네트워크 환경과 보안 제한이 있을 수 있습니다. 이로 인해 Deno Deploy에서는 기본적으로 로컬 환경과 같은 방식으로 MongoDB에 연결하고 통신할 수 없습니다.

해결 방법은 다음과 같습니다:

Deno Deploy용 백엔드 서비스 사용: Deno Deploy에서는 MongoDB와 같은 외부 서비스에 대한 직접적인 액세스가 제한되므로, Deno Deploy를 위한 백엔드 서비스를 사용해야 합니다. 이는 MongoDB를 대신하여 Deno Deploy와 호환되는 데이터베이스 서비스를 사용하거나, Deno Deploy와 통합되는 데이터베이스 서비스를 활용하는 것을 의미합니다.

Deno Deploy의 환경 변수 사용: Deno Deploy에서는 환경 변수를 사용하여 데이터베이스 정보를 설정할 수 있습니다. .env 파일을 사용하여 Deno Deploy에 필요한 데이터베이스 연결 정보를 제공하고, Deno Deploy의 Deno.env.get() 메서드를 사용하여 해당 정보에 액세스할 수 있습니다.

Deno Deploy용 데이터베이스 모듈 사용: Deno Deploy에서는 Deno 모듈을 사용하여 데이터베이스에 액세스할 수 있는 특정 모듈을 활용해야 합니다. MongoDB와의 통신을 위해 Deno Deploy에서 공식적으로 지원하는 모듈이 있는지 확인하고, 그에 맞게 코드를 수정해야 합니다.

위의 해결 방법 중 하나를 선택하여 Deno Deploy에서 MongoDB와의 통신을 활성화해야 합니다. 이를 통해 Deno Deploy 환경에서도 예상대로 작동하는 애플리케이션을 실행할 수 있을 것입니다.

- ChatGPT

시도해볼 수 있는 선택지는 꽤 다양해 보입니다.

가장 확실하고 노력이 많이 필요해보이는 선택지는 파이어베이스로 마이그레이션입니다. 레퍼런스도 어느정도 있습니다. 그래서 현실적이지만 나중에 시도해보고 싶습니다.

Connect to Firebase

Deno Crash Course #5 - Integrating Deno with Firebase Firestore

리스크 테이킹을 하고 DenoKV를 선택하는 것도 방법입니다. 저 혼자 사용하고 저장할 데이터의 유형이 텍스트인데 1GB까지 지원한다고 합니다.

Deno KV

언젠가 할 기능은 맞지만 현재 시도는 안 해보겠습니다. 없다고 viable 안 한것은 아닙니다.

그래서 Deno.env.get()로 활용하는 방법으로 우회부터 시도하겠습니다.

erfanium / deno-deploy-mongo

저랑 비슷한 이슈가 있었는데 해결한 사람 같습니다.

const { CARD_API_KEY, MONGO_URI } = config();

기존입니다.

const MONGO_URI = Deno.env.get('MONGO_URI')!;
const CARD_API_KEY = Deno.env.get('CARD_API_KEY')!;

적용입니다.

이렇게 하면 Deon deploy 환경에서 확실하게 환경변수를 읽을 수 있습니다.

미세팁: config 동시 Deno.env.get 활용
const CARD_API_KEY = Deno.env.get('CARD_API_KEY')! || config()['CARD_API_KEY'];
const MONGO_URI = Deno.env.get('MONGO_URI')! || config()['MONGO_URI'];

개발환경과 배포환경 모두 환경변수를 접근할 수 있는 방법입니다.

ReferenceError: Worker is not defined

에러가 바뀌었습니다. 또 개발환경에서도 다른 버그가 발생했습니다. 이렇게 되면 다른 문제도 있습니다. 환경 변수가 개발환경에서 undefined가 됩니다. 그래서 다른 시도로 일단 환경변수 접근할 때는 기존 모듈을 그대로 활용하지만 URL환경변수로 제공하는 시도했습니다.

에러는 동일합니다.

일단 GPT 선생님에게 질문하니까 이런 답변을 받았습니다.

주어진 정보를 토대로 분석해보면, Deno Deploy에서 발생하는 문제로 보입니다. 코드의 일부에서 Worker와 관련된 오류가 발생하고 있으며, 이는 Deno Deploy 환경에서 Worker를 지원하지 않기 때문입니다. Deno Deploy에서는 웹 워커(worker)를 사용할 수 없으므로 Worker를 사용하는 코드는 오류를 발생시킬 수 있습니다.

해결책으로는 다음과 같은 접근 방법을 고려할 수 있습니다:

  1. Worker를 사용하지 않는 대안 채택: Deno Deploy에서는 Worker 대신에 다른 방식으로 비동기 처리를 구현해야 합니다. Deno Deploy는 각 요청에 대해 독립적인 실행 환경을 생성하고, 이를 통해 요청을 처리합니다. 따라서 Deno Deploy에 맞게 코드를 리팩토링하여 Worker를 사용하지 않고 비동기 작업을 처리해야 합니다.

  2. Oak 미들웨어와 라우터 사용: Oak 프레임워크를 사용하여 Deno 애플리케이션을 개발할 때, Deno Deploy에서도 Oak를 사용할 수 있습니다. Deno Deploy에서 Oak를 사용하려면, 코드를 Deno Deploy 환경에 맞게 구성해야 합니다. Oak의 Application 클래스를 사용하여 Deno Deploy에 적합한 웹 애플리케이션을 작성하고, Deno Deploy의 제한사항을 고려하여 코드를 수정해야 합니다.

  3. Deno Deploy의 문서와 예제 참조: Deno Deploy에는 공식 문서와 예제가 제공되므로, 해당 자료를 참조하여 Deno Deploy 환경에서 애플리케이션을 개발하는 방법을 학습할 수 있습니다. 문서와 예제를 통해 Deno Deploy의 제한사항과 사용 방법에 대한 이해를 높이고, 문제를 해결하는 데 도움을 받을 수 있습니다.

위의 접근 방식을 고려하여 코드를 수정하고 Deno Deploy 환경에 맞게 애플리케이션을 구성하면, Worker를 사용하는 부분을 대체하고 원활한 실행을 이끌어 낼 수 있습니다.

hallucination 현상이 발생한 것 같습니다. Oak를 사용하고 있다는 컨텍스트를 제공했는데 이런 답변을 받았습니다. 또 문서참조를 요구하고 있습니다.

그래서 다음 타자 phind에게 질문했습니다.

The error message "ReferenceError: Worker is not defined" suggests that there is an issue with the worker being defined in the deployment environment. Here are some possible solutions to this problem:

  • Check if the worker is defined in the deployment environment. Workers are not defined in all environments, so you need to make sure that the worker is defined in the environment you are deploying to. If it is not defined, you can try using a different environment that supports workers.
  • Try importing the required modules in the worker file. If the worker file is missing some required modules, it can cause the "ReferenceError: Worker is not defined" error. Make sure that all the required modules are imported in the worker file.
  • Check if the worker file is being loaded correctly. The worker file needs to be loaded correctly in order for the worker to be defined. Make sure that the worker file is being loaded correctly and there are no errors in the loading process.
  • Refactor the code to avoid using workers. If none of the above solutions work, you can try refactoring the code to avoid using workers. Workers are not always necessary, so you can try using a different approach to accomplish the same task.

In the given code, there is no worker being defined, so the issue might be caused by other factors. Here are some possible solutions to the problem:

  • Check if the deployment environment has the necessary dependencies installed. The error message might be caused by missing dependencies in the deployment environment. Make sure that all the necessary dependencies are installed in the deployment environment.
  • Check if the environment variables are set correctly. The error message might be caused by incorrect environment variables in the deployment environment. Make sure that all the environment variables are set correctly in the deployment environment.
  • Check if the server is running correctly. The error message might be caused by a problem with the server. Make sure that the server is running correctly and there are no errors in the server logs.
  • Try running the code in a different environment. If none of the above solutions work, you can try running the code in a different environment to see if the problem is specific to the deployment environment.

Overall, the "ReferenceError: Worker is not defined" error message suggests that there is an issue with the worker being defined in the deployment environment. However, in the given code, there is no worker being defined, so the issue might be caused by other factors.

같은 모델인데 다른 답변을 해줍니다.

다른 가설들

잠시 생각하고 다른 가설들을 세워봤습니다.

  1. Deno deploy 환경에서 Automatic 설정은 기본이 READ-only(get 요청만 가능)
  2. Allow-net으로 외부 요청이 허용되어 있지 않습니다.

1번의 문제는 get이 아닌 요청을 보내면 미들웨어 역할을 할 함수가 동작하기 전에 에러 피드백을 줘야 합니다. 하지만 토큰이 없다는 post, patch, delete 요청을 보면 미들웨어는 저의 의도대로 동작합니다.

그렇다면 이런 실험도 가능합니다. 외부에 요청할 API를 잠시 테스트용 API를 만들고 검증하는 전략입니다.

JSON Placeholder에 요청을 보내고 미들웨어 서버가 응답하는지 확인하면 됩니다.

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => console.log(json));

외부 요청을 보낼 수 있게 되었습니다.

import { Router } from 'https://deno.land/x/oak@v12.4.0/mod.ts';

const router = new Router();

router
.get('/api', (ctx) => {
ctx.response.body = 'hello world!';
})
.get('/api/secret', async (ctx) => {
await fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => {
ctx.response.status = 200;
ctx.response.body = { json };
});
});

export default router;

임시로 이렇게 라우터에 추가하고 배포했습니다. 외부 네트워크 요청에는 문제가 없습니다.

다른 방법도 있습니다. 1가지 안 해본 것은 삭제하고 다시 배포하는 것입니다. 해보고 같은 에러 상태가 유지됩니다.

버그 발원지 면적축소

배포환경에서 특이한 점을 발견했습니다.

{
"success": false,
"msg": "Error: 이미 가입한 아이디입니다."
}

환경변수 설정을 올바르게 해도 기존에 있던 버그는 그대로 유지되고 있었습니다. 가입하지 않은 아이디로 로그인 POST 요청을 보내면 위와 같은 응답을 줍니다.

async function signup({ request, response }: Context) {
try {
if (!request.hasBody) {
throw Error('body가 없습니다.');
}
const input = await request.body().value;

if (!input.email || !input.password) {
throw Error('이메일 혹은 비밀 번호가 없습니다.');
}

const document = await mongoAPI.getUser(input.email);

if (document !== null) {
throw Error('이미 가입한 아이디입니다.');
} else {
const passwordSalt = await genSalt(8);
const passwordHash = await hash(input.password, passwordSalt);

response.status = 201;
response.body = await mongoAPI.postUser({
email: input.email,
passwordHash,
passwordSalt,
});
}
} catch (error) {
response.status = 400;
response.body = {
success: false,
msg: `${error}`,
};
}
}

이 코드를 보면 응답을 받은 document는 존재하지 않은 id를 검색하면 null을 반환합니다. 하지만 이미 가입한 아이디입니다.라는 피드백을 주는 것을 보면 documentnull이 아닌 다른 값입니다. 하지만 가입한적이 없는 이메일을 body로 넣고 요청을 보냈는데 이런 요청을 보냈습니다.

console.log(null !== undefined); // true

저의 추론은 documentundefined을 반환합니다.

  async getUser(email: string) {
try {
const result = await fetch(`${this.baseURL}/findOne`, {
...this.options,
body: JSON.stringify({
...this.userBody,
filter: { email },
}),
});
const data = await result.json();
return data.document;
} catch (error) {
return error;
}
}

그 다음은 이 부분이 문제입니다. data.documentundefined가 된다는 것입니다. 여기서 다음 의문은 data는 일단 객체로 존재한다는 것입니다. 그 중에 document라는 프로퍼티가 없다는 것입니다.

그래서 수정을 해보고 직접 실험을 해봤습니다.

async function signup({ request, response }: Context) {
try {
if (!request.hasBody) {
throw Error('body가 없습니다.');
}
const input = await request.body().value;

if (!input.email || !input.password) {
throw Error('이메일 혹은 비밀 번호가 없습니다.');
}

const document = await mongoAPI.getUser(input.email);
if (document === undefined) throw Error('document is undefined');

if (document !== null) {
throw Error('이미 가입한 아이디입니다.');
} else {
const passwordSalt = await genSalt(8);
const passwordHash = await hash(input.password, passwordSalt);

response.status = 201;
response.body = await mongoAPI.postUser({
email: input.email,
passwordHash,
passwordSalt,
});
}
} catch (error) {
response.status = 400;
response.body = {
success: false,
msg: `${error}`,
};
}
}

이렇게 수정하고 다시 merge했습니다. 하지만 이미 가입한 아이디입니다. 응답은 동일했습니다.

이렇게 해도 응답이 이미 가입한 아이디입니다.이었습니다. 그래서 응답을 직접 관측하기로 했습니다.

async function signup({ request, response }: Context) {
try {
if (!request.hasBody) {
throw Error('body가 없습니다.');
}
const input = await request.body().value;

if (!input.email || !input.password) {
throw Error('이메일 혹은 비밀 번호가 없습니다.');
}

const document = await mongoAPI.getUser(input.email);
if (document === undefined) throw Error('document is undefined');

if (document !== null) {
throw Error(`이미 가입한 아이디입니다. ${document.email}`);
} else {
const passwordSalt = await genSalt(8);
const passwordHash = await hash(input.password, passwordSalt);

response.status = 201;
response.body = await mongoAPI.postUser({
email: input.email,
passwordHash,
passwordSalt,
});
}
} catch (error) {
response.status = 400;
response.body = {
success: false,
msg: `${error}`,
};
}
}

가설이 검증 되었습니다. undefined가 맞았습니다. document는 아마 어떤 객체일 것 같습니다.

mongoDB get 요청

  .get("/api/secret", async (ctx) => {
const mongoAPI = MongoAPI.getInstance();

ctx.response.status = 200;
ctx.response.body = await mongoAPI.getAllCards();
});

일단 이렇게 작성했습니다. getAllCards 요청을 보내면 보안 취약점입니다. 프로덕션 코드에서 이렇게 하면 큰일납니다.

하지만 일단 merge하고 ENV 설정 보니까 일단 동작합니다. get 요청에는 성공적입니다.

가설: 문제는 bcrypt이다.

mongoDB 클라우드와 연결문제가 아니라는 것을 검증했습니다. ReferenceError: Worker is not defined에러가 발생한다는 것은 통신 문제가 아니라 라이브러리 활용 문제라는 것을 파악했습니다. bcrypt은 상당히 작은 규모의 라이브러리입니다.

추론이지만 worker 모듈이 존재합니다.

BCrypt를 확인하니까 다음과 같은 설명이 있습니다.

The Async implementation requires WebWorkers which require --allow-net to import Deno standard modules from inside the Worker.

가설은 검증되었습니다. 다음은 대응책입니다. 2가지 선택지가 있습니다. 하나는 권장하지 않는 비동기화 다른 하나는 권장하는 방식을 따르지만 --allow-net으로 배포를 설정하는 것입니다.

검색을 더 해보는 과정에서 저랑 비슷한 에러를 겪은 사람들이 있습니다.

Deno Deploy Support

import {
hash as hashPromise,
hashSync,
compare as comparePromise,
compareSync,
} from 'https://deno.land/x/bcrypt/mod.ts';

export const isRunningInDenoDeploy = Deno.permissions?.query === undefined; // This is crude check for if the code in running in Deno Deploy. It works for now but may not work in the future.

export const hash: typeof hashPromise = isRunningInDenoDeploy
? (plaintext: string, salt: string | undefined = undefined) =>
new Promise((res) => res(hashSync(plaintext, salt)))
: hashPromise;
export const compare: typeof comparePromise = isRunningInDenoDeploy
? (plaintext: string, hash: string) =>
new Promise((res) => res(compareSync(plaintext, hash)))
: comparePromise;

이런 코드를 발견했습니다. 저는 genSalt도 추가해야 하기 때문에 다음으로 응용했습니다.

util/customBcrypt.ts
import {
hash as hashPromise,
hashSync,
compare as comparePromise,
compareSync,
genSalt as genSaltPromise,
genSaltSync,
} from 'https://deno.land/x/bcrypt@v0.4.1/mod.ts';

export const isRunningInDenoDeploy = Deno.permissions?.query === undefined; // This is crude check for if the code in running in Deno Deploy. It works for now but may not work in the future.

export const hash: typeof hashPromise = isRunningInDenoDeploy
? (plaintext: string, salt: string | undefined = undefined) =>
new Promise((res) => res(hashSync(plaintext, salt)))
: hashPromise;

export const compare: typeof comparePromise = isRunningInDenoDeploy
? (plaintext: string, hash: string) =>
new Promise((res) => res(compareSync(plaintext, hash)))
: comparePromise;

export const genSalt: typeof genSaltPromise = isRunningInDenoDeploy
? (plaintext: number | undefined) =>
new Promise((res) => res(genSaltSync(plaintext)))
: genSaltPromise;

저는 이렇게 변경했습니다. deno의 한계를 배운 부분입니다.

하지만 또 다른 문제가 또 있었습니다. 바로 const isRunningInDenoDeploy = Deno.permissions?.query === undefined부분입니다.

이슈 아래를 보니까 const isRunningInDenoDeploy = (globalThis as any).Worker === undefined;로 해결하신 분도 있었습니다.

적용하고 성공했습니다.

아래는 다른 가설 후보 및 문제 해결 방법들이었습니다.

  • glitch로 배포하기
  • mongo client 활용하기
  • 가설: 응답을 받은 클라이언트 즉 thunder client에 worker가 없어서 생기는 문제인가?
  • docker가 필요합니다.

해결: Workerless Bcrypt

util/customBcrypt.ts
import {
hash as hashPromise,
hashSync,
compare as comparePromise,
compareSync,
genSalt as genSaltPromise,
genSaltSync,
} from 'https://deno.land/x/bcrypt@v0.4.1/mod.ts';

const isRunningInDenoDeploy = (globalThis as any).Worker === undefined; // This is crude check for if the code in running in Deno Deploy. It works for now but may not work in the future.

export const hash: typeof hashPromise = isRunningInDenoDeploy
? (plaintext: string, salt: string | undefined = undefined) =>
new Promise((res) => res(hashSync(plaintext, salt)))
: hashPromise;

export const compare: typeof comparePromise = isRunningInDenoDeploy
? (plaintext: string, hash: string) =>
new Promise((res) => res(compareSync(plaintext, hash)))
: comparePromise;

export const genSalt: typeof genSaltPromise = isRunningInDenoDeploy
? (plaintext: number | undefined) =>
new Promise((res) => res(genSaltSync(plaintext)))
: genSaltPromise;

이렇게 util 함수를 만들고 소비하는 곳에 모두 이 함수로 교체했던니 문제가 풀렸습니다. 당연히 애매입니다.

Deno에 Worker가 생기기 전까지 아마 유지해야 할 것 같습니다. 또 라이브러리 운영자도 해당 이슈를 남긴다고 했습니다.

학습: 디버깅은 공학보단 과학처럼

  1. 개발환경과 배포환경은 다릅니다. 환경을 통일시키는 방법을 찾도록 합니다.
  • 이런 이유로 Docker로 환경을 통합해야 합니다. 그래서 Docker를 배우고 싶어졌습니다.
  • Worker가 처음부터 없던 개발환경에서 구현했으면 Deno.env로 개발하고 bcrypt도 바로 디버깅했을 것입니다.
  1. 개발환경과 배포환경 각각 환경변수를 읽기 위한 방법이 다릅니다.
// 개발환경
import { config } from 'https://deno.land/x/dotenv@v3.2.2/mod.ts';

const { CARD_API_KEY, MONGO_URI } = config();
// 배포환경
const MONGO_URI = Deno.env.get('MONGO_URI')!;
const CARD_API_KEY = Deno.env.get('CARD_API_KEY')!;

환경마다 환경변수를 접근하는 방법을 다르게 해야 합니다.

  1. 배포는 조기에 합시다.

조기에 배포했으면 이 버그는 조기에 대응가능 했을 것입니다.

  1. 디버깅할 때는 글을 쓰면서 합니다.
  • 디버깅은 공학보단 과학입니다.
  • 가설을 설정하고 검증 혹은 기각하면서 현상을 분석합니다.
  • 기록해가면서 진행하면 했던 행동을 나중에 다시 반복하지 않습니다.
  • 가설을 기각하면 버그의 발원지 면적을 축소한 것입니다.
  • 가설을 검증하면 버그를 찾고 대응할 수 있는 것입니다.
  1. Deno에는 Worker가 없습니다.
  • Worker가 없어서 bcrypt 라이브러리를 애매로 개조해야 합니다.