This is translation of  Slack Next-gen Platform – External API Calls” written by Kaz (SDK Engineering & DevRel at Slack), I do not include any personal opinion here except guessing when meaning of original text is not clear for me)

이 글은 슬랙의 SDK Engineering / DevRel 인 Kaz 의 글을 번역한것입니다. 저의 개인 의견은 들어가지 않으며, 일부 원문의 의미가 애매한 경우에만 부연 설명을 달았습니다.


이번 튜토리얼에서는 Slack 의 차세대 플랫폼 내에서 동작하는 커스텀 펑션내에서 외부 API 호출을 하는 방법에 대해서 배워보겠습니다.

이번에는 DeepL’s 의 텍스트 변역 API 를 사용해서 번역을 수행하는 커스텀 펑션을 만들어 볼것입니다.

Prerequisites

차세대 플랫폼을 접하는 것이 처음이시라면, 이전의 튜토리얼인 “The Simplest Hello World” 부터 읽어주세요. 요약하면 유료 Slack 워크스페이스와 해당 워크스페이스에서 “beta feature” 를 사용할 수 있는 권한이 필요합니다. 가지고 계시다면 Slack CLI 와 워크스페이스를 연결하기만 하면 됩니다.

준비되었다면 앱을 한번 만들어보도록 하죠. 시작해보겠습니다.

Create a Blank Project

slack create 명령어를 사용하여, 새로운 프로젝트를 시작할 수 있습니다. 이 튜토리얼에서는 아무것도 없는 상태에서 앱을 만들어보겠습니다. “Blank Project” 를 선택하여 주십시요.

$ slack create
? Select a template to build from:

  Hello World
  A simple workflow that sends a greeting

  Scaffolded project
  A solid foundational project that uses a Slack datastore

> Blank project
  A, well.. blank project

  To see all available samples, visit github.com/slack-samples.

프로젝트가 생성되면, slack run 명령어가 정상적으로 동작하는지 확인해보세요. 이 명령어는 “dev” 버전의 앱을 워크스페이스에 설치합니다. 이 앱의 봇 유저가 생성되고, 이 봇은 API 호출을 위한 봇 토큰 값을 가지고 있습니다.

$ cd affectionate-panther-654
$ slack run
? Choose a workspace  seratch  T03E94MJU
   App is not installed to this workspace

Updating dev app install for workspace "Acme Corp"

  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
  seratch of Acme Corp
Connected, awaiting events

Connected, awaiting events 로그 메세지를 보신다면, 이 앱이 성공적으로 워크스페이스에 연결된 것입니다. “Ctrl +C” 를 눌러 로컬 앱 프로세스를 종료합니다.

Create a Workflow and its Link Trigger

이전 튜토리얼에서 이미 다뤘기 때문에, 커스텀 펑션에 대한 기본적인 내용은 넘어가겠습니다. 혹시 아직 읽어보지 않았다면 일독을 권합니다.

이 튜토리얼에서는, 비어있는 프로젝트에 두개의 파일을 추가할 것입니다.

  • 워크플로와 프리거가 정의되어있는 workflow_and_trigger.ts
  • 외부 API 를 호출하는 커스텀 펑션이 정의되어있는 function.ts

workflow_and_trigger.ts 부터 시작해보죠. 이후에 워크플로에 추가적으로 3개의 펑션 스텝을 추가할 것입니다.

// -------------------------
// Workflow definition
// -------------------------
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";

export const workflow = DefineWorkflow({
  callback_id: "translator-workflow",
  title: "Translator Workflow",
  input_parameters: {
    properties: {
      // The channel to post the translation result
      channel_id: { type: Schema.slack.types.channel_id },
      // Need this to open a form
      interactivity: { type: Schema.slack.types.interactivity },
    },
    required: ["channel_id", "interactivity"],
  },
});

// TODO: Add function steps to the workflow

// -------------------------
// Trigger Definition
// -------------------------
import { Trigger } from "deno-slack-api/types.ts";

// This trigger starts the workflow when an end-user clicks the link
const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "Translator Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    // The channel where an end-user clicks the link
    channel_id: { value: "{{data.channel_id}}" },
    // This input is required for opening a form within the workflow
    interactivity: { value: "{{data.interactivity}}" },
  },
};

// As long as the trigger object is default exported,
// you can generate a trigger with this code:
// $ slack triggers create --trigger-def ./workflow_and_trigger.ts
export default trigger;

까먹으실 수도 있을듯 하여 다시한번 언급합니다. manifest.ts 에 워크플로를 추가하세요. 또한 지금 만들고 있는 펑션은 외부의 API 엔드포인트(api-free.deepl.com(프리티어 사용시) and api.deepl.com) 로 HTTP 요청을 보낼 것이기 때문에 두개의 도메인을 outgoingDomains 에 반드시 추가해주어야 합니다.

import { Manifest } from "deno-slack-sdk/mod.ts";
import { workflow as TranslatorWorkflow } from "./workflow_and_trigger.ts";

export default Manifest({
  name: "zealous-elk-261",
  description: "Translate text in Slack",
  icon: "assets/default_new_app_icon.png",
  // Add your workflow to this list
  workflows: [TranslatorWorkflow],
  // All the domains except slack.com must be listed
  outgoingDomains: [
    "api-free.deepl.com", // only for free tier usage
    "api.deepl.com",
  ],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

slack run 을 실행하여 별다른 에러가 발생하지 않으면, slack triggers create –trigger-def ./workflow_and_trigger.ts 명령어를 실행하여 link 트리거를 생성합니다. 두개의 옵션을 보실텐데 이전과 마찬가지로 (dev) 가 달린것을 선택해주십시요.

$ slack triggers create --trigger-def ./workflow_and_trigger.ts
? Choose an app  [Use arrows to move, type to filter]
   seratch  T03E94MJU
   App is not installed to this workspace

>  seratch (dev)  T03E94MJU
   zealous-elk-261 (dev) A04DHV08MPF

별다른 문제가 없으면 link 트리거의 주소가 생성될 것입니다.

$ slack triggers create --trigger-def ./workflow_and_trigger.ts
? Choose an app  seratch (dev)  T03E94MJU
   zealous-elk-261 (dev) A04DHV08MPF

Trigger created
   Trigger ID:   Ft04DEBXXXX
   Trigger Type: shortcut
   Trigger Name: Translator Trigger
   URL: https://slack.com/shortcuts/Ft04DEBXXXXX/YYYY

위에서 생성된 link 트리거 주소를 채널에게 메세지로 보내면, 워크플로를 시작할 수 있습니다.

Add Three Functions to the Workflow

워크플로와 link 트리거가 준비되었습니다. 그렇지만 이 워크플로는 아직 별다른 기능을 가지고 있지 않습니다. 이 워크플로에 이제 커스텀 펑션을 포함하여 3개의 펑션을 추가할 것입니다.

  1. 빌트인 OpenForm 펑션을 추가하여 엔드유저의 입력값을 받아옵니다.(오리지널 텍스트와, 어떤 언어로 변환할지)
  2. 번역를 수행하는 커스텀 펑션 translate 를 추가합니다.
  3. 빌트인 SendMessage 펑션을 추가하여 번역된 결과물을 채널로 보냅니다.

빌트인 펑션들은 가져다 쓰면 됩니다. 남아있는 일은 translate 펑션을 추가하는 일입니다. function.ts 파일을 만듭시다.

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { FunctionSourceFile } from "https://deno.land/x/deno_slack_source_file_resolver@0.1.5/mod.ts";

// The metadata definition for the translator function
export const def = DefineFunction({
  callback_id: "translate",
  title: "Translate",
  description: "Translate text using DeepL's API",
  // This example code uses a 3rd party module "deno_slack_source_file_resolver"
  // to automatically resolve the relative path of this source file.
  source_file: FunctionSourceFile(import.meta.url),
  input_parameters: {
    properties: {
      text: { type: Schema.types.string },
      source_lang: { type: Schema.types.string }, // optional
      target_lang: { type: Schema.types.string },
    },
    required: ["text", "target_lang"],
  },
  output_parameters: {
    properties: { translated_text: { type: Schema.types.string } },
    required: ["translated_text"],
  },
});

export default SlackFunction(def, async ({ inputs, env }) => {
  // When running a dev version of your app,
  // placing `.env` file with variables is the way to configure `env`.
  // As for the deployed prod one, you need to run
  // `slack env add DEEPL_AUTH_KEY (value)` before running the app's workflow.
  const authKey = env.DEEPL_AUTH_KEY;
  if (!authKey) {
    // Since it's impossible to continue in this case, this function returns an error.
    const error =
      "DEEPL_AUTH_KEY env value is missing! Please add `.env` file for `slack run`. If you've already deployed this app, `slack env add DEEPL_AUTH_KEY (value)` command configures the env variable for you.";
    return { error };
  }
  // Build an HTTP request towards DeepL's text translation API
  const subdomain = authKey.endsWith(":fx") ? "api-free" : "api";
  const deeplApiUrl = `https://${subdomain}.deepl.com/v2/translate`;
  const body = new URLSearchParams();
  body.append("auth_key", authKey);
  body.append("text", inputs.text);
  if (inputs.source_lang) { // this input is optional
    body.append("source_lang", inputs.source_lang.toUpperCase());
  }
  body.append("target_lang", inputs.target_lang.toUpperCase());
  // When there is no Deno library to perform external API calls,
  // simply using the built-in `fetch` function is recommended.
  const response = await fetch(deeplApiUrl, {
    method: "POST",
    headers: {
      "content-type": "application/x-www-form-urlencoded;charset=utf-8",
    },
    body,
  });
  const status = response.status;
  if (status != 200) {
    const body = await response.text();
    const error = `DeepL API error (status: ${status}, body: ${body})`;
    return { error };
  }
  // When the translation succeeds, the response body is JSON data.
  const result = await response.json();
  if (!result || result.translations.length === 0) {
    const error = `Translation failed: ${JSON.stringify(result)}`;
    return { error };
  }
  // When it's successful, the outputs must include "translated_text" as it's required.
  return { outputs: { translated_text: result.translations[0].text } };
});

이 펑션은 DeepL 의 유효한 API 인증키를 필요로 합니다. 아직 DeepL 의 개발자 계정이 없다면 (개발자 계정은 일반 계정과 다릅니다.) 등록 페이지로 가서 만드세요. 계정이 활정화 되었다면, DeepL 개발자 계정 페이지에서 DeepL API 인증키를 받을 수 있습니다.

코드내에 인증키를 직접 넣고 싶어하실 수도 있겠지만 (왜냐면 쉬우니까요), 소스 코드내에 키를 하드코딩 하는 것은 보안관점에서 권장하지 않습니다. 대안으로 env 변수를 사용하여 인증 및 기밀 정보들을 전달할 수 있습니다.

slack run 명령어를 사용하여 (dev) 버전의 앱을 실행할 때, 프로젝트의 루트 디렉토리에 .env 파일을 넣어 둘 수 있습니다. .env 파일을 만들어서 아래 내용을 저장하세요.

DEEPL_AUTH_KEY=[your auth key here]

slack deploy 명령어를 사용하여 Prod 버전의 앱을 배포할때는 .env 파일을 사용하지 않습니다. 대신 배포한 직후에 slack env add DEEPL_AUTH_KEY [your auth key here] 를 실행하여 주십시요.

자, 이제 워크플로에 이 펑션들을 넣어보도록 하겠습니다. workflow_and_trigger.ts 파일을 열어주시고, 다음 코드를 추가하여 주십시요.

// Receive the original text and which language to translate into
const formStep = workflow.addStep(Schema.slack.functions.OpenForm, {
  title: "Run DeepL Translator",
  // To use this built-in function,
  // either a preceding function or the trigger of workflow
  // must provide interactivity in inputs.
  interactivity: workflow.inputs.interactivity,
  submit_label: "Translate",
  // The fields are similar to Block Kit, but some differences exist.
  // Refer to https://api.slack.com/future/forms#type-parameters for details.
  fields: {
    elements: [
      {
        name: "text",
        title: "Original Text",
        type: Schema.types.string,
      },
      {
        name: "target_lang",
        title: "Target Language",
        type: Schema.types.string,
        description: "Select the language to translate into",
        enum: [
          "English",
          "Japanese",
          "Korean",
          "Chinese",
          "Italian",
          "French",
          "Spanish",
        ],
        choices: [
          { value: "en", title: "English" },
          { value: "ja", title: "Japanese" },
          { value: "kr", title: "Korean" },
          { value: "zh", title: "Chinese" },
          { value: "it", title: "Italian" },
          { value: "fr", title: "French" },
          { value: "es", title: "Spanish" },
        ],
        default: "en",
      },
    ],
    required: ["text", "target_lang"],
  },
});

// Import your translator function and add it to this workflow
import { def as translate } from "./function.ts";
const translateStep = workflow.addStep(translate, {
  text: formStep.outputs.fields.text,
  target_lang: formStep.outputs.fields.target_lang,
});

// Post the translation result using the built-in message function
workflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: workflow.inputs.channel_id,
  message:
    `>${formStep.outputs.fields.text}\n${translateStep.outputs.translated_text}`,
});

아마 아직은 OpenForm 펑션이 익숙하지 않을 것입니다. 지금은 걱정하실 필요 없습니다. 엔드유저가 Slack UI 상에서 Form 을 사용하는 것과 비슷합니다. 그렇지만, 커스텀 form 을 만들때에는 몇가지 배워야 할것들이 있습니다만. 다른 튜토리얼에서 다뤄보겠습니다.

자 이제 워크플로가 준비되었습니다. link 트리거를 사용하여 시작해보도록 하겠습니다. Translate 버튼을 클릭하면, 팝업 모달을 보실 수 있습니다.

만약 영어로 변환을 하기 원하신다면, 다른 언어를 쓰시고, 번역할 언어로 영어를 선택하세요. 만약 영어만 하실 줄 안다면, 영어를 입력하고 영어가 아닌 다른 언어를 선택하시면 됩니다.

오리지날 텍스트와 타겟 언어가 입력되었다면, 번역된 결과물이 바로 포스팅 될 것입니다. 만약 아무일도 일어나지 않는다면 slack run 명령어가 실행중인 터미널을 확인해보세요. 또한 워크플로 실행이 실패 했다면 slackbot 으로부터 DM 메세지도 받으실 겁니다.

More Sophisticated Translator App

만약 좀더 세련된 형태의 앱을 찾고 계시다면, 제가 최근에 공개한 오피셜 샘플 앱을 한번 봐보세요.

https://github.com/slack-samples/deno-message-translator

어떻게 동작하는지 말씀드려보면, 엔드유저가 국기 이모지를 리액션으로 추가하면, 앱이 해당 국가의 언어로 번역을 해서 쓰레드에 결과물을 남겨줍니다.

공용어가 여러개인 경우 국기를 선택했을 때 어떤 언어로 변환될지에 대한 다른 의견이 있을 것으로 생각합니다. 만약 그렇다면 직접 소스코드에서 바꾸시면 됩니다.

Wrapping Up

이번 튜토리얼에서는 다음과 같은 내용을 배워보았습니다.

  • 외부 API 호출을 위한 펑션 생성
  • manitest.ts 내에서 외부 도메인 접근 허용
  • 워크플로와 펑션을 위하여 env 변수 추가

이 프로젝트는 다음 링크에서도 확인하실 수 있습니다. https://github.com/seratch/slack-next-generation-platform-tutorials/tree/main/03_External_API_Calls

이번 튜토리얼도 즐거우셨으면 좋겠네요. 마찬가지로 피드백이나 코멘트가 있으시다면 트위터 (@seratch) 로 연락주시거나, 여기 남겨주세요.

Happy hacking with Slack’s next-generation platform 🚀

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다