
๐ Overview
์ฌ๋ ๊ฐ์ธ ์ฑ๋์์ ์ฌ๋ฌ ๊ธฐ์ ๋ธ๋ก๊ทธ๋ฅผ RSS ํผ๋๋ก ๊ตฌ๋ ์ค์ธ๋ฐ์, ๋ฌธ์ ์ ์ฒด ๋ด์ฉ์ ์์ฝํด์ ๊ฐ ๊ธ์ ๋๊ธ๋ก ์ ๋ฆฌํด ๋๋ฉด, ๋ค์ํ ์ํฐํด์ ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ผ๋ก ์ดํดํ๋ ๋ฐ ๋์์ด ๋ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋๋ค.
ํนํ ์ผ๋ถ ํ ํฌ ๋ธ๋ก๊ทธ๋ ๊ธฐ์ ๋ฟ ์๋๋ผ ๋์์ธ, ๋ฐ์ดํฐ, ํ๋ก๋ํธ ๋ฑ ์ฌ๋ฌ ๋ถ์ผ์ ์ฝํ ์ธ ๋ ํจ๊ป ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์, ์์ฝ๊ณผ ํจ๊ป ์นดํ ๊ณ ๋ฆฌ๋ณ๋ก ๋ถ๋ฅ๊น์ง ํด์ฃผ๋ฉด ํจ์ฌ ์ ์ฉํ ๊ฒ ๊ฐ์ต๋๋ค.
์ฌ๋ AI ์์ฝ ๊ธฐ๋ฅ์ด Pro ์๊ธ์ ์์๋ง ์ ๊ณต๋๋ค ๋ณด๋, ์ง์ ๊ฐ๋จํ ์ฑ์ ๊ตฌํํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
๐ Architecture

RSS ๊ตฌ๋
์ค์ธ ๋ธ๋ก๊ทธ์ ์๋ก์ด ๊ธ์ด ๋ฐํ๋๋ฉด ์ด๋ฅผ ํน์ ๊ตฌ๋
์ฑ๋์ ๊ณต์ ํฉ๋๋ค.
์ด๋ RSS Slack App ์ ํตํด ๊ตฌ์ฑํฉ๋๋ค.
๐ก RSS ์ฑ ์ค์น ๋ฐ ๊ตฌ๋ ๋ฐฉ๋ฒ์ ๋ค์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
๐ ๋ฌธ์ ๋งํฌ
ํน์ ์ฑ๋์ ์๋ก์ด ๋ฉ์์ง๊ฐ ๋ฑ๋ก๋๋ฉด ํด๋น ์ด๋ฒคํธ๋ฅผ ๊ฐ์งํ๊ณ AWS Lambda์์ ์ด์ ์ค์ธ Slack Bot์ ์ ๋ฌํฉ๋๋ค.
Slack Bot ์ ๋ฉ์์ง ๋ด์ ์ฒจ๋ถ๋ ๋ฌธ์ ๋งํฌ๋ฅผ ํตํด์ ์๋ณธ ๋ฌธ์๋ฅผ ํ์ํ๊ณ ์์ฝ๋ ๋ฉ์์ง๋ฅผ ์์ฑํด์ ์ฑ๋์ ์๋ตํฉ๋๋ค.
๐ Slack Bot Message Handling
Slack Bolt for JavaScript
Slack ์๋ Node.js ํ๊ฒฝ์์ ๋์ํ๋ ๋ด์ ๊ตฌํํ ์ ์๋ ๊ณต์ ํ๋ ์์ํฌ์ธ Bolt ๊ฐ ์ ๊ณต๋ฉ๋๋ค.
ํด๋น ํ๋ ์์ํฌ๋ฅผ ํตํด ์ํ๋ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋ ๋ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค.
๐กBolt ๋ฅผ ํตํด Slack Bot์ ๊ตฌ์ฑํ๋ ๋ณด๋ค ์์ธํ ๋ด์ฉ์ ๋ค์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์.
๐ Bolt for JavaScript
Slack Events API
Slack Events API ๋ slack bot์ด Slack ํ๋์ ๊ฐ์งํ๊ณ ์ ์ ํ ์๋ต์ ์ ๊ณตํ ์ ์๋๋ก ํ๋ API์
๋๋ค.
Socket ๋ชจ๋์ public HTTP endpoint ๋ชจ๋ ์ค ํ๋๋ฅผ ์ ํํด์ ์ ์ฉํ ์ ์์ต๋๋ค.
์ ๋ ๋ณ๋์ ๋ฐฉํ๋ฒฝ์ ์ฌ์ฉํ๊ณ ์์ง ์๊ณ production ์ฑ์ AWS Lambda ํ๊ฒฝ์์ ์ด์ํ ์์ ์ด๊ธฐ ๋๋ฌธ์ HTTP ๋ฐฉ์์ ์ฌ์ฉํ์ต๋๋ค.
๋ก์ปฌ ๊ฐ๋ฐํ๊ฒฝ์ ๊ฒฝ์ฐ public HTTP endpoint๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด์ reverse proxy๋ฅผ ์ฌ์ฉํด์ผ ํ๋๋ฐ ์ด๋ฅผ ์ํด์ ngrok ์๋ฃจ์
์ ํ์ฉํ ์ ์์ต๋๋ค.
๋ ๋ชจ๋์ ์ฐจ์ด์ ์ ์ด ๋ฌธ์ ์์ ํ์ธํ ์ ์์ต๋๋ค.

Event message Parsing
ํ์ฌ RSS ๊ตฌ๋ ์ค์ธ ๋ธ๋ก๊ทธ์ ๋ฐํ ์์์ ๋ค์๊ณผ ๊ฐ์ ํ์์ผ๋ก ์ ๋ฌ๋๊ณ ์์ต๋๋ค.

message ๋ฆฌ์ค๋์์๋ ์ด ๋ฉ์์ง๋ฅผ ๋ค์๊ณผ ๊ฐ์ ํ์์ผ๋ก ์ ๋ฌ๋ฐ๊ณ ์์ต๋๋ค.
{
text: '<https://medium.com/daangn/%EC%9B%B9%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B9%85-%EA%B0%9C%EC%84%A0%EA%B8%B0-10e819a39a1d?source=rss----4505f82a2dbd---4|์น์ฑ ์๋ฒ ๋ก๊น
๊ฐ์ ๊ธฐ>\n' +
'์๋
ํ์ธ์! ๋๋ค์ํํ ํ๋ก ํธ์๋ ์์ง๋์ด ์ธํด ๋ง์ปค(Linker)์์. ์ ํฌ ํ์ ๋๋ค ์ ๋ณด์ ์ด์ผ๊ธฐ๋ฅผ ์์ ๋กญ๊ฒ ๋๋๋ ์ปค๋ฎค๋ํฐ, ๋๋ค์ํ์ ๋ง๋ค๊ณ ์์ด์.๋๋ค์ํ์ ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ ์ฑ๋ฅ์ด ์ข์ง ์๊ฑฐ๋ ์น๋ทฐ ๋ก๋ฉ์ด ๋๋ฆฐ ํ๊ฒฝ์์๋ ์ฌ์ฉ์์๊ฒ ์ํํ ๊ฒฝํ์ ์ ๊ณตํ๊ธฐ ์ํด Streaming SSR*์ ๋์
ํ์ด์. ์ด๋ฐ ๊ตฌ์กฐ์์ ๋๋ฒ๊น
์ ์์ํ๊ฒ ํ๊ธฐ ์ํด ์๋ฒ ๋ก๊น
์ ์ ๊ทน์ ์ผ๋ก ํ์ฉํด ์์ฃ . ๊ทธ๋ฐ๋ฐ ์๋น์ค ๊ท๋ชจ๊ฐ ์ปค์ง๋ฉด์ ์๋ฒ ๋ด๋ถ์์ ์ถ๋ ฅ๋๋ ๋ก๊น
์ด ๋ง์์ง์ ๋ช ๊ฐ์ง ๋ฌธ์ ๋ฅผ ์ง๋ฉดํ๊ฒ ๋์ด์.',
// ...
}
๋ฉ์์ง ๊ตฌ์กฐ๋ฅผ ๋ฏ์ด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ์ ๋ชฉ ์์ญ์ ๋งํฌ ํ์์ธ ๊ฒฝ์ฐ
|๊ตฌ๋ถ์๋ฅผ ํตํด์ ๋งํฌ์ ํ์ํ์์ ๊ตฌ๋ถ - ๋ฉ์์ง์ ๋ณธ๋ฌธ์ด ํฌํจ๋ ๊ฒฝ์ฐ์๋ ์ ๋ชฉ ์์ญ์
\n(๊ฐํ๋ฌธ์)๋ฅผ ์ถ๊ฐํ๊ณ ์ดํ์ ๋ณธ๋ฌธ ๋ด์ฉ์ด concat ๋์ด์ ์ ๋ฌ
์ฐ๋ฆฌ์๊ฒ ํ์ํ ๊ฒ์ ๋ณธ๋ฌธ์ด ์๋ ์๋ฌธ์ ๋งํฌ์ด๊ธฐ ๋๋ฌธ์ ์ ์ ํ ํ์ฑ์ด ํ์ํฉ๋๋ค.
์ฐ์ \n ๋ฌธ์๋ฅผ ๊ธฐ์ค์ผ๋ก split ํ ๋ค์ title, content ์์ญ์ ๊ตฌ๋ถํด ์ค๋๋ค.
const [title, content] = rawMessage.split('\n')
์ด์ title ์๋ ๋ค์๊ณผ ๊ฐ์ ํ์์ผ๋ก ๋ฌธ์์ด์ด ๋ค์ด์ค๊ฒ ๋ฉ๋๋ค.
<https://medium.com/daangn/%EC%9B%B9%EC%95%B1-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B9%85-%EA%B0%9C%EC%84%A0%EA%B8%B0-10e819a39a1d?source=rss----4505f82a2dbd---4|์น์ฑ ์๋ฒ ๋ก๊น
๊ฐ์ ๊ธฐ>
์ฌ๊ธฐ์ ์ค์ ๋งํฌ ์์ญ๋ง ์ถ์ถํ๋ ๋ฐฉ๋ฒ์ผ๋ก ์ ๊ทํํ์์ ํ์ฉํ ์ ์์ต๋๋ค.
์ ๋ฐฉํ์(look-ahead)๊ณผ ํ๋ฐฉํ์(look-behind)์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
const sourceLink = title.match(/(?<=<).*(?=\|)/g)[0]
Message replies
Bolt API์์ ํน์ ๋ฉ์์ง์ ๋๊ธ์ ์ถ๊ฐํ๋ ค๋ฉด say ํธ์ถ ์ thread_ts ๋ฅผ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.
์ฌ๊ธฐ์ thread_ts ๋ ๋๊ธ์ ์ถ๊ฐํ ์ค๋ ๋์ ๊ณ ์ ID ๊ฐ์ด๋ฉฐ ์ด๋ ๋ฉ์์ง ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ํตํด ์กฐํํ ์ ์์ต๋๋ค.
app.message(/.*/, async ({ event, message, say }) => {
// ...
await say({
text: `์ถ์ถ๋ ์๋ฌธ ๋งํฌ๋ ๋ค์๊ณผ ๊ฐ๋ค์: ${sourceLink}`,
thread_ts: message.ts,
})
})

๋ณด๋ค ์์ธํ ๋ด์ฉ์ Message ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
๐ LLM์ ํ์ฉํด์ ์ํฐํด ์์ฝํ๊ธฐ
LLM Model ์ ์
๋ฌด๋ฃ๋ก ์ฌ์ฉํ ์ ์๋ ๊ณต๊ฐ LLM API ํ๋ซํผ๋ค์ด ๋ช ๊ฐ์ง ์์ต๋๋ค.
ํฌ๊ฒ GitHub Models ์ Google AI Studio ๋ฅผ ๊ณ ๋ฏผํด ๋ดค๋๋ฐ, Google์์ ์ ๊ณตํ๋ Gemini ๋ชจ๋ธ์ด Free Tier์์ ์ฒ๋ฆฌํ ์ ์๋ input token์ ๋ฒ์๊ฐ ๋ ์ปค์ ์ด๊ฑธ ํ์ฉํด ๋ณด๊ธฐ๋ก ํ์ต๋๋ค.
์ฌ์ฉ๋ฒ์ ๋ํ ์์ธํ ๋ด์ฉ์ ๋ค์ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ์ฃผ์ธ์.
https://ai.google.dev/gemini-api/docs
Gemini API | Google AI for Developers
Gemini Developer API ๋ฌธ์ ๋ฐ API ์ฐธ์กฐ
ai.google.dev
Prompt ๊ตฌ์ฑ
์ต์ด์๋ Gemini์ ์ญํ ์ ๋ถ์ฌํ๊ณ ์๋ต ์ ๊ณ ๋ คํด์ผ ํ ์ฌํญ, ๊ทธ๋ฆฌ๊ณ ์๋ต ํ์์ ์ ๋ฌํด ์ฃผ๊ณ ์ฐธ์กฐํด์ผ ํ ๋ฌธ์ ๋งํฌ๋ฅผ ์ ๋ฌํด ์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ ๊ทผํ์ต๋๋ค.
const response = await ai.models.generateContent({
model: 'gemini-2.5-pro',
contents: generatePrompt(resourceLink),
config: {
systemInstruction: 'You are senior software engineer and you are summarizing a blog post for your team.',
thinkingConfig: {
temperature: 0, // lower temperature for deterministic output
thinkingBudget: 0, // disable thinking and reduce latency cause it is not needed when summarizing (maybe?)
},
tools: [{ urlContext: {} }],
},
})
function generatePrompt(resourceLink) {
return `
[Summary Guidelines]
Please follow the guidelines below when summarizing:
• Read the blog post at the link provided and summarize the key points as if you’re sharing it with your team.
• Avoid unnecessary embellishments—keep the writing clear and concise.
• Use simple and clear language.
• Do not infer or add any information that is not explicitly mentioned in the blog post.
• Do not include any additional information or context outside of the blog post.
• No greetings or closing remarks are necessary.
• Response in Korean.
• Do not use bold or italics in the response.
[Exception Handling Guidelines]
If the blog post is not accessible or does not contain sufficient information, please respond with:
• "์ํฐํด์ ์ ๊ทผํ ์ ์์ต๋๋ค."
[Summary Format]
Please format the summary as follows:
• Infer Category of the blog post from content.
• Category should be one of the following with backticks:
'AI', 'Web Development', 'Mobile Development', 'DevOps', 'Cloud Computing', 'Data Science', 'Cybersecurity', 'Software Engineering', 'Programming Languages', 'Frameworks and Libraries'.
• Summarize content should be in bullet points.
• This should be after the category.
• Use bullet points for each key point.
• Each bullet point should be concise.
• Use headings to separate different sections of the summary.
• Use sub-categorize with sub-headings (ex. h2, h3 ...).
• Use sub-categorize actively to make the summary more readable.
[Summary Example]
Here is an example of how the summary should look. follow the same format:
• Category: \`AI\`
• Heading 1
- Sub-point 1
- Sub-point 2
• Heading 2
- Sub-point 1
- Sub-point 2
... and so on.
[Blog Post to Summarize]
Read the blog post at the following link:
• ${resourceLink}
`
}
Gemini API์๋ URL Context๋ผ๋ ์คํ์ ์ธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ ์์ต๋๋ค.
https://ai.google.dev/gemini-api/docs/url-context
URL context | Gemini API | Google AI for Developers
Live API์ ์๋ก์ด ๋ค์ดํฐ๋ธ ์ค๋์ค ๋ชจ๋ธ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์์ธํ ์์๋ณด๊ธฐ ์ด ํ์ด์ง๋ Cloud Translation API๋ฅผ ํตํด ๋ฒ์ญ๋์์ต๋๋ค. ์๊ฒฌ ๋ณด๋ด๊ธฐ URL context URL ์ปจํ ์คํธ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด URL ํ
ai.google.dev
์ด๋ฅผ ํตํด ์ค์ ์ํฐํด์ ๋ด์ฉ์ ์ ๊ทผํด์ ์ฝํ ์ธ ์ ๋ํ ์์ฝ๋ณธ์ ์์ฑํ ์ ์์ต๋๋ค.
๋ค๋ง ์์ง ์คํ์ ์ธ ๋จ๊ณ๋ผ์ ๊ทธ๋ฐ์ง ์ผ๋ถ ์ํฐํด์ ์ ํจํ ์ ๊ทผ์ ํ์ง ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ์์์ต๋๋ค.

url-context ๋๊ตฌ๋ฅผ ํตํด ์ํฐํด์ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํ ์ํ๋ผ๋ฉด ์ง์ ์ํฐํด์ ํฌ๋กค๋งํด์ ์์ฝ๋ณธ์ ์์ฑํ๊ณ ์ถ์์ต๋๋ค.
function calling์ ํตํด์ ๊ธฐ๋ฅ์ ์ธ๋ถํํ๋ ค๊ณ ํ๋๋ฐ, Gemini API๋ ํ์ฌ function calling๊ณผ URL context(๋๋ tool) ๊ธฐ๋ฅ์ ๋์์ ํ ์์ฒญ์์ ์ฌ์ฉํ ์ ์๋ ์ ์ฝ์ฌํญ์ด ์์ต๋๋ค.
await ai.models.generateContent({
model: 'gemini-2.5-flash',
config: {
tools: [{ urlContext: {}, functionDeclarations: [someFunction] }] // โ
}
})
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ํ๋กฌํํธ ๋จ๊ณ๋ฅผ ๋๋๋ ๋ฐฉ์์ผ๋ก ์ ๊ทผํ์ต๋๋ค.
- ๋จผ์ url-context tool์ ์ด์ฉํด์ ํด๋น ์ฝํ ์ธ ์ ์ ๊ทผํ ์ ์๋์ง ํ๋จํ๋ค.
- ์ ๊ทผ์ด ๊ฐ๋ฅํ ์ํ๋ผ๋ฉด, ๊ธฐ์กด url-context๋ฅผ ํ์ฉํ๋ ๋ฐฉ์์ผ๋ก ์์ฝ
- ์ ๊ทผ์ด ์ด๋ ค์ด ์ํ๋ผ๋ ์๋ต ๋ฉ์์ง๋ฅผ ๋ฐ์ผ๋ฉด ์ง์ ํฌ๋กค๋งํด์ ์์ฝ๋ณธ์ ํ๋กฌํํธ์ ์ ๋ฌ
- ํฌ๋กค๋ฌ๋ก ์ ๊ทผ์ด ์ ๋๋ ์ํ๋ผ๋ฉด ์์ฝ๋ถ๊ฐ ์ฒ๋ฆฌ
const response = await ai.models.generateContent({
/** */
})
if (isFailureResponse(response)) {
/** ํฌ๋กค๋ง ํ ์ํฐํด ์๋ฒ ๋ฉํ์ฌ ์ฌ์์ฒญ */
} else {
/** ์ ์์๋ต ๋ฐํ */
}
crawlee ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ด์ฉํด์ ๊ฐ๋จํ ํฌ๋กค๋ง ์ฝ๋๋ฅผ ์ถ๊ฐํ์ต๋๋ค.
export async function crawling({ url, onFinish, onFailure }) {
const crawler = createCrawler({ onFinish, onFailure })
await crawler.run([url])
}
function createCrawler({ onFinish, onFailure }) {
const config = new Configuration({
/**
* ๐ก NOTE:
* Do not persist storage in this crawler.
* This is because the crawler is used in a serverless environment,
* and we do not want to persist any data between invocations.
* If you need to persist data, use a different crawler with a persistent storage configuration.
*/
persistStorage: false,
})
const crawler = new CheerioCrawler(
{
async requestHandler({ $ }) {
const article = $('article').text()
onFinish(article)
},
async failedRequestHandler() {
onFailure()
},
},
config
)
return crawler
}
์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์์ ๊ฐ invokation ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์์์ ์ผ๋ก ์ ์งํ ํ์๋ ์๊ธฐ ๋๋ฌธ์ persistStorage ์ต์ ์ ์ฌ์ฉํ์ง ์์์ต๋๋ค.
Slack Event API ์๋ ์๋ต ์ง์ฐ์๊ฐ 3์ด ์ด๋ด์ 2xx status code๋ก ์๋ตํ์ง ์์ผ๋ฉด ์ด๋ฒคํธ ์๋ต ์คํจ๋ก ๊ฐ์ฃผํ๊ณ 3๋ฒ ์ฌ์๋ํฉ๋๋ค.
์ด๋ฌํ ์ ์ฝ ๋๋ฌธ์ ์ํฐํด์ ์กฐํํ๊ณ ์ฝํ ์ธ ๋ฅผ ์์ฝํ๋ ๊ณผ์ ์ด ๊ธธ์ด์ง๋ ๊ฒฝ์ฐ ์๋ํ์ง ์๊ฒ Lambda ํจ์๊ฐ ์ฌ์๋๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
Lambda๋ ์คํ ์๊ฐ๊ณผ ํ์์ ๋ฐ๋ผ ๊ณผ๊ธ๋๊ธฐ ๋๋ฌธ์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ํ์๊ฐ ์์์ต๋๋ค.
์์ฒญ์ ๋ํ ์๋ต์ ์ฒ๋ฆฌํ๋ ํ๋ก์ธ์ค์ ์ค์ ์์ฝ์ ์ฒ๋ฆฌํ๋ ํ๋ก์ธ์ค๋ฅผ ๋ถ๋ฆฌํ๋ ๋ฑ์ผ๋ก ํด๊ฒฐํ ์๋ ์์ง๋ง ๋ ๋จ์ํ๊ฒ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ Message ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ ์ ๋ฌ๋ฐ์ ์ ์๋ ์ฌ์๋ ํ์๋ฅผ ์ฐธ๊ณ ํ๊ธฐ๋ก ํ์ต๋๋ค.
Slack Event API๋ ์์ฒญ ์ฌ์๋ ์์ HTTP ํค๋์ x-slack-retry-num ๋ฅผ ์ถ๊ฐํด์ ์ ๋ฌํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ๊ฐ์ bolt sdk ๋ด์์๋ context.retryNum ์ผ๋ก ์ฐธ์กฐ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
// bolt-js ์์ค์ฝ๋
// https://github.com/slackapi/bolt-js/blob/9337e0616f058f1459ef1ad922dfba38048dac3b/src/receivers/HTTPModuleFunctions.ts#L13
export const extractRetryNumFromHTTPRequest = (req: IncomingMessage): number | undefined => {
let retryNum: number | undefined;
const retryNumHeaderValue = req.headers['x-slack-retry-num'];
if (retryNumHeaderValue === undefined) {
retryNum = undefined;
} else if (typeof retryNumHeaderValue === 'string') {
retryNum = Number.parseInt(retryNumHeaderValue, 10);
} else if (Array.isArray(retryNumHeaderValue) && retryNumHeaderValue.length > 0) {
retryNum = Number.parseInt(retryNumHeaderValue[0], 10);
}
return retryNum;
};
context.retryNum ๊ฐ 0๋ณด๋ค ํฐ ๊ฒฝ์ฐ ์ฌ์๋ ์์ฒญ์ผ๋ก ๊ฐ์ฃผํ๊ณ early-return ํฉ๋๋ค.
app.message(/.*/, async ({ context, event, message, say }) => {
const isRetry = isRetryEvent(context)
if (isRetry) {
// If this is a retry event, we do not want to process it again.
// This is to prevent duplicate processing of the same event.
return
}
// ...
}
function isRetryEvent(context) {
const isRetry = (context.retryNum ?? 0) > 0
return isRetry
}
๐ Slack Bot ๋ฐฐํฌํ๊ธฐ
serverless framework
AWS Lambda๋ฅผ ํตํด์ ์๋ฒ๋ฆฌ์ค ํ๊ฒฝ์ ์ง์ ๊ตฌ์ถํ๋ ๋์ AWS CloudFormation์ ํตํด์ ํ ํ๋ฆฟ ํ์ผ์ ํตํด ๋ฆฌ์์ค๋ฅผ ํ ๋น ๋ฐ ๊ด๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ๊ณตํด ์ค๋๋ค.
serverless.yaml ํ์ผ์ ์์ฑํ ๋ค์ ํ์ํ ๋ฆฌ์์ค ํ์
์ ์ ์ํฉ๋๋ค.
service: slack-bot-clippy
frameworkVersion: '4'
useDotenv: true
provider:
name: aws
runtime: nodejs22.x
region: ap-northeast-2
environment:
SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET}
SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN}
AWS_CLIENT_TIMEOUT: 30000
GEMINI_API_KEY: ${env:GEMINI_API_KEY}
functions:
slack:
handler: app.handler
maximumRetryAttempts: 0
timeout: 20
events:
- http:
path: slack/events
method: post
plugins:
- serverless-offline
๋ก์ปฌํ๊ฒฝ์์ AWS Lambda ๋์์ ํ์ธํ๊ธฐ ์ํด์ serverless-offline ํ๋ฌ๊ทธ์ธ์ ํ์ฉํ ์ ์์ต๋๋ค.
ํด๋น ํ๋ฌ๊ทธ์ธ์ ๋ก์ปฌํ๊ฒฝ์์ API Gateway์ Lambda ๋์์ ์๋ฎฌ๋ ์ด์ ํฉ๋๋ค.
๋ฐฐํฌ๋ ๋ค์ ์ปค๋ฉ๋๋ก ๊ฐํธํ๊ฒ ๊ฐ๋ฅํฉ๋๋ค.
serverless deploy

์์ฑ๋ endpoint๋ฅผ Slack App ์ค์ ์์ Event Subscription Request URL๋ก ์ง์ ํด ์ฃผ๋ฉด ๋ฉ๋๋ค.
ํด๋น endpoint๋ API Gateway๋ฅผ ํตํด ์ ๊ณต๋๋ API Endpoint๊ฐ ๋ฉ๋๋ค.

์ดํ์ RSS ๊ตฌ๋
์ค์ธ ์ฑ๋์ clippy ๋ด์ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.
๐ ๋ง๋ฌด๋ฆฌ

์ด๋ฒ ๊ธฐํ์ AWS Lambda ํ๊ฒฝ์ ์ง์ ๊ตฌ์ฑํ๊ณ , LLM ๋ชจ๋ธ์ ํ์ฉํ ๊ฐ๋จํ ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ง๋ค์ด๋ดค์ต๋๋ค.
๋น๊ต์ ๋จ์ํ ์์ฝ ์์ ์ด๋ผ์ ํ๋กฌํํธ ํ๋ ์ค๋ก ๋๋ ์ค ์์๋๋ฐ, ์ํ๋ ํํ๋ก ์๋ต์ ์ ํํ๊ฒ ์ป์ด๋ด๋ ค๋ฉด ์์๋ณด๋ค ํจ์ฌ ์ธ๋ฐํ ํ๋กฌํํธ ์ค๊ณ๊ฐ ํ์ํ์ต๋๋ค.
'๐จโ๐ป web.dev > devops' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| GitHub Actions ๋ฅผ ํ์ฉํ release bot ๋ง๋ค๊ธฐ (0) | 2023.04.24 |
|---|---|
| SVN ์ ์ด์ฉํ ํ์๊ด๋ฆฌ PART.2 - SVN ๋ธ๋์น ์ ๋ต ์ธ์ฐ๊ธฐ (1) | 2022.03.01 |
| SVN ์ ์ด์ฉํ ํ์๊ด๋ฆฌ PART.1 - SVN ์ด๋? (0) | 2022.03.01 |
๐ฌ ๋๊ธ