[official skill] Slack GIF ์์ฑ๊ธฐ
์ด ์คํฌ์ Slack์ ์ต์ ํ๋ ์ ๋๋ฉ์ด์ GIF๋ฅผ PIL(Pillow) ๊ธฐ๋ฐ์ผ๋ก ์์ฑํ๊ณ ์ต์ ํํ๋ ๋๊ตฌ์ ๋๋ค.
๐ ์ฐธ๊ณ : ์ด ๋ฌธ์๋ ๊ณต์ Claude Code ๋ฌธ์์ ์๋ฌธ + ๋ ํผ๋ฐ์ค์ ๋๋ค.
- ์๋ฌธ ๋งํฌ๋ฅผ ์ง์ ์ฌ์ฉํ์ ๋ ๋ฉ๋๋ค
- ์ด ํ์ผ์
.claude/skills/ํ์์ ๋ณต์ฌํ์ฌ ์คํฌ๋ก ๋ฑ๋กํ ์๋ ์์ต๋๋ค(2026.03.15 ๊ธฐ์ค)
Slack GIF ์์ฑ๊ธฐ
Slack์ ์ต์ ํ๋ ์ ๋๋ฉ์ด์ GIF๋ฅผ ์์ฑํ๊ธฐ ์ํ ์ ํธ๋ฆฌํฐ์ ์ง์์ ์ ๊ณตํ๋ ํดํท์ ๋๋ค.
Slack ์๊ตฌ์ฌํญ
ํฌ๊ธฐ:
- ์ด๋ชจ์ง GIF: 128x128 (๊ถ์ฅ)
- ๋ฉ์์ง GIF: 480x480
ํ๋ผ๋ฏธํฐ:
- FPS: 10-30 (๋ฎ์์๋ก ํ์ผ ํฌ๊ธฐ๊ฐ ์์)
- ์์: 48-128 (์ ์์๋ก ํ์ผ ํฌ๊ธฐ๊ฐ ์์)
- ์ฌ์ ์๊ฐ: ์ด๋ชจ์ง GIF์ ๊ฒฝ์ฐ 3์ด ๋ฏธ๋ง ์ ์ง
ํต์ฌ ์ํฌํ๋ก์ฐ
from core.gif_builder import GIFBuilder
from PIL import Image, ImageDraw
# 1. ๋น๋ ์์ฑ
builder = GIFBuilder(width=128, height=128, fps=10)
# 2. ํ๋ ์ ์์ฑ
for i in range(12):
frame = Image.new('RGB', (128, 128), (240, 248, 255))
draw = ImageDraw.Draw(frame)
# PIL ๊ธฐ๋ณธ ๋ํ์ผ๋ก ์ ๋๋ฉ์ด์
๊ทธ๋ฆฌ๊ธฐ
# (์, ๋ค๊ฐํ, ์ ๋ฑ)
builder.add_frame(frame)
# 3. ์ต์ ํ์ ํจ๊ป ์ ์ฅ
builder.save('output.gif', num_colors=48, optimize_for_emoji=True)
๊ทธ๋ํฝ ๊ทธ๋ฆฌ๊ธฐ
์ฌ์ฉ์ ์ ๋ก๋ ์ด๋ฏธ์ง ์์
์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ ๊ฒฝ์ฐ, ๋ค์ ์ค ์ด๋ค ์๋์ธ์ง ํ์ ํ์ธ์:
- ์ง์ ์ฌ์ฉ (์: "์ด๊ฒ์ ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ง๋ค์ด์ค", "์ด๊ฒ์ ํ๋ ์์ผ๋ก ๋ถํ ํด์ค")
- ์๊ฐ์ผ๋ก ์ฌ์ฉ (์: "์ด๊ฒ๊ณผ ๋น์ทํ ๊ฒ์ ๋ง๋ค์ด์ค")
PIL์ ์ฌ์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ๊ณ ์์ ํ์ธ์:
from PIL import Image
uploaded = Image.open('file.png')
# ์ง์ ์ฌ์ฉํ๊ฑฐ๋, ์์/์คํ์ผ ์ฐธ๊ณ ์ฉ์ผ๋ก๋ง ์ฌ์ฉ
์ฒ์๋ถํฐ ๊ทธ๋ฆฌ๊ธฐ
์ฒ์๋ถํฐ ๊ทธ๋ํฝ์ ๊ทธ๋ฆด ๋๋ PIL ImageDraw ๊ธฐ๋ณธ ๋ํ์ ์ฌ์ฉํ์ธ์:
from PIL import ImageDraw
draw = ImageDraw.Draw(frame)
# ์/ํ์
draw.ellipse([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3)
# ๋ณ, ์ผ๊ฐํ, ๋ชจ๋ ๋ค๊ฐํ
points = [(x1, y1), (x2, y2), (x3, y3), ...]
draw.polygon(points, fill=(r, g, b), outline=(r, g, b), width=3)
# ์
draw.line([(x1, y1), (x2, y2)], fill=(r, g, b), width=5)
# ์ง์ฌ๊ฐํ
draw.rectangle([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3)
์ฌ์ฉํ์ง ๋ง์ธ์: ์ด๋ชจ์ง ํฐํธ(ํ๋ซํผ๋ง๋ค ๋์์ด ๋ค๋ฆ) ๋๋ ์ด ์คํฌ์ ๋ฏธ๋ฆฌ ํจํค์ง๋ ๊ทธ๋ํฝ์ด ์๋ค๊ณ ๊ฐ์ ํ๋ ๊ฒ.
๊ทธ๋ํฝ์ ๋ณด๊ธฐ ์ข๊ฒ ๋ง๋ค๊ธฐ
๊ทธ๋ํฝ์ ๊ธฐ๋ณธ์ ์ธ ๊ฒ์ด ์๋๋ผ ์ธ๋ จ๋๊ณ ์ฐฝ์์ ์ผ๋ก ๋ณด์ฌ์ผ ํฉ๋๋ค. ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
๋๊บผ์ด ์ ์ฌ์ฉ - ์ค๊ณฝ์ ๊ณผ ์ ์ ํญ์ width=2 ์ด์์ ์ค์ ํ์ธ์. ์์ ์ (width=1)์ ๊ฑฐ์น ๊ณ ์๋ง์ถ์ด์ ์ผ๋ก ๋ณด์
๋๋ค.
์๊ฐ์ ๊น์ด ์ถ๊ฐ:
- ๋ฐฐ๊ฒฝ์ ๊ทธ๋ผ๋ฐ์ด์
์ฌ์ฉ (
create_gradient_background) - ๋ณต์กํ ๋๋์ ์ํด ์ฌ๋ฌ ๋ํ ๊ฒน์น๊ธฐ (์: ํฐ ๋ณ ์์ ์์ ๋ณ)
๋ํ์ ๋ ํฅ๋ฏธ๋กญ๊ฒ ๋ง๋ค๊ธฐ:
- ๋จ์ํ ์๋ง ๊ทธ๋ฆฌ์ง ๋ง๊ณ - ํ์ด๋ผ์ดํธ, ๋ง, ํจํด์ ์ถ๊ฐํ์ธ์
- ๋ณ์ ๋ฐ๊ด ํจ๊ณผ๋ฅผ ์ค ์ ์์ต๋๋ค (๋ค์ ๋ ํฌ๊ณ ๋ฐํฌ๋ช ํ ๋ฒ์ ์ ๊ทธ๋ฆฌ๊ธฐ)
- ์ฌ๋ฌ ๋ํ์ ์กฐํฉํ์ธ์ (๋ณ + ๋ฐ์ง์, ์ + ๋ง)
์์์ ์ฃผ์:
- ์๋๊ฐ ์๊ณ ๋ณด์ ๊ด๊ณ์ ์์ ์ฌ์ฉ
- ๋๋น ์ถ๊ฐ (๋ฐ์ ๋ํ์ ์ด๋์ด ์ค๊ณฝ์ , ์ด๋์ด ๋ํ์ ๋ฐ์ ์ค๊ณฝ์ )
- ์ ์ฒด ๊ตฌ์ฑ ๊ณ ๋ ค
๋ณต์กํ ๋ํ (ํํธ, ๋๊ฝ ๋ฑ)์ ๊ฒฝ์ฐ:
- ๋ค๊ฐํ๊ณผ ํ์์ ์กฐํฉ ์ฌ์ฉ
- ๋์นญ์ ์ํด ํฌ์ธํธ๋ฅผ ์ ์คํ๊ฒ ๊ณ์ฐ
- ์ธ๋ถ ์ฌํญ ์ถ๊ฐ (ํํธ์๋ ํ์ด๋ผ์ดํธ ๊ณก์ , ๋๊ฝ์๋ ์ ๊ตํ ๊ฐ์ง)
์ฐฝ์์ ์ด๊ณ ๋ํ ์ผํ๊ฒ ๋ง๋์ธ์! ์ข์ Slack GIF๋ ์์ ๊ทธ๋ํฝ์ด ์๋๋ผ ์ธ๋ จ๋๊ฒ ๋ณด์ฌ์ผ ํฉ๋๋ค.
์ฌ์ฉ ๊ฐ๋ฅํ ์ ํธ๋ฆฌํฐ
GIFBuilder (core.gif_builder)
ํ๋ ์์ ์กฐ๋ฆฝํ๊ณ Slack์ ์ต์ ํํฉ๋๋ค:
builder = GIFBuilder(width=128, height=128, fps=10)
builder.add_frame(frame) # PIL Image ์ถ๊ฐ
builder.add_frames(frames) # ํ๋ ์ ๋ชฉ๋ก ์ถ๊ฐ
builder.save('out.gif', num_colors=48, optimize_for_emoji=True, remove_duplicates=True)
Validators (core.validators)
GIF๊ฐ Slack ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋์ง ํ์ธํฉ๋๋ค:
from core.validators import validate_gif, is_slack_ready
# ์์ธ ๊ฒ์ฆ
passes, info = validate_gif('my.gif', is_emoji=True, verbose=True)
# ๋น ๋ฅธ ํ์ธ
if is_slack_ready('my.gif'):
print("Ready!")
Easing Functions (core.easing)
์ ํ ๋์ ๋ถ๋๋ฌ์ด ๋ชจ์ :
from core.easing import interpolate
# 0.0์์ 1.0๊น์ง์ ์งํ๋
t = i / (num_frames - 1)
# ์ด์ง ์ ์ฉ
y = interpolate(start=0, end=400, t=t, easing='ease_out')
# ์ฌ์ฉ ๊ฐ๋ฅ: linear, ease_in, ease_out, ease_in_out,
# bounce_out, elastic_out, back_out
Frame Helpers (core.frame_composer)
์ผ๋ฐ์ ์ธ ์๊ตฌ์ฌํญ์ ์ํ ํธ์ ํจ์:
from core.frame_composer import (
create_blank_frame, # ๋จ์ ๋ฐฐ๊ฒฝ
create_gradient_background, # ์์ง ๊ทธ๋ผ๋ฐ์ด์
draw_circle, # ์ ํฌํผ
draw_text, # ๊ฐ๋จํ ํ
์คํธ ๋ ๋๋ง
draw_star # 5๊ฐ ๋ณ
)
์ ๋๋ฉ์ด์ ๊ฐ๋
ํ๋ค๋ฆผ/์ง๋
์ค์ค๋ ์ด์ ์ผ๋ก ๊ฐ์ฒด ์์น ์คํ์ :
math.sin()๋๋math.cos()๋ฅผ ํ๋ ์ ์ธ๋ฑ์ค์ ํจ๊ป ์ฌ์ฉ- ์์ฐ์ค๋ฌ์ด ๋๋์ ์ํด ์์ ๋๋ค ๋ณํ ์ถ๊ฐ
- x ๋ฐ/๋๋ y ์์น์ ์ ์ฉ
ํ์ค/ํํธ๋นํธ
๋ฆฌ๋ฌ๊ฐ ์๊ฒ ๊ฐ์ฒด ํฌ๊ธฐ ์กฐ์ :
math.sin(t * frequency * 2 * math.pi)๋ฅผ ์ฌ์ฉํ์ฌ ๋ถ๋๋ฌ์ด ํ์ค- ํํธ๋นํธ์ ๊ฒฝ์ฐ: ๋ ๋ฒ์ ๋น ๋ฅธ ํ์ค ํ ๋ฉ์ถค (์ฌ์ธํ ์กฐ์ )
- ๊ธฐ๋ณธ ํฌ๊ธฐ์ 0.8์์ 1.2 ์ฌ์ด๋ก ์ค์ผ์ผ
๋ฐ์ด์ค
๊ฐ์ฒด๊ฐ ๋จ์ด์ง๊ณ ํ์ด์ค๋ฆ:
- ์ฐฉ์ง์
interpolate()์easing='bounce_out'์ฌ์ฉ - ๋ํ(๊ฐ์)์
easing='ease_in'์ฌ์ฉ - ๋งค ํ๋ ์๋ง๋ค y ์๋๋ฅผ ์ฆ๊ฐ์์ผ ์ค๋ ฅ ์ ์ฉ
ํ์ /๋กํ ์ดํธ
์ค์ฌ์ ๊ธฐ์ค์ผ๋ก ๊ฐ์ฒด ํ์ :
- PIL:
image.rotate(angle, resample=Image.BICUBIC) - ํ๋ค๋ฆผ ํจ๊ณผ: ์ ํ ๋์ ์ฌ์ธํ๋ฅผ ๊ฐ๋์ ์ฌ์ฉ
ํ์ด๋ ์ธ/์์
์ ์ง์ ์ผ๋ก ๋ํ๋๊ฑฐ๋ ์ฌ๋ผ์ง:
- RGBA ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ์ํ ์ฑ๋ ์กฐ์
- ๋๋
Image.blend(image1, image2, alpha)์ฌ์ฉ - ํ์ด๋ ์ธ: ์ํ 0์์ 1
- ํ์ด๋ ์์: ์ํ 1์์ 0
์ฌ๋ผ์ด๋
๊ฐ์ฒด๋ฅผ ํ๋ฉด ๋ฐ์์ ์์น๋ก ์ด๋:
- ์์ ์์น: ํ๋ ์ ๊ฒฝ๊ณ ๋ฐ
- ๋ ์์น: ๋ชฉํ ์์น
- ๋ถ๋๋ฌ์ด ์ ์ง๋ฅผ ์ํด
interpolate()์easing='ease_out'์ฌ์ฉ - ์ค๋ฒ์ํธ ํจ๊ณผ:
easing='back_out'์ฌ์ฉ
์ค
์ค ํจ๊ณผ๋ฅผ ์ํ ์ค์ผ์ผ ๋ฐ ์์น ์กฐ์ :
- ์ค ์ธ: 0.1์์ 2.0์ผ๋ก ์ค์ผ์ผ, ์ค์ ํฌ๋กญ
- ์ค ์์: 2.0์์ 1.0์ผ๋ก ์ค์ผ์ผ
- ๋๋ผ๋งํฑํ ํจ๊ณผ๋ฅผ ์ํด ๋ชจ์ ๋ธ๋ฌ ์ถ๊ฐ ๊ฐ๋ฅ (PIL ํํฐ)
ํญ๋ฐ/ํํฐํด ๋ฒ์คํธ
๋ฐ๊นฅ์ชฝ์ผ๋ก ๋ฐฉ์ฌ๋๋ ํํฐํด ์์ฑ:
- ๋๋ค ๊ฐ๋์ ์๋๋ก ํํฐํด ์์ฑ
- ๊ฐ ํํฐํด ์
๋ฐ์ดํธ:
x += vx,y += vy - ์ค๋ ฅ ์ถ๊ฐ:
vy += gravity_constant - ์๊ฐ์ด ์ง๋จ์ ๋ฐ๋ผ ํํฐํด ํ์ด๋ ์์ (์ํ ๊ฐ์)
์ต์ ํ ์ ๋ต
ํ์ผ ํฌ๊ธฐ๋ฅผ ์ค์ด๋ผ๋ ์์ฒญ์ด ์์ ๋๋ง, ๋ค์ ๋ฐฉ๋ฒ ์ค ๋ช ๊ฐ์ง๋ฅผ ๊ตฌํํ์ธ์:
- ํ๋ ์ ์ ์ค์ด๊ธฐ - FPS ๋ฎ์ถ๊ธฐ (20 ๋์ 10) ๋๋ ์ฌ์ ์๊ฐ ๋จ์ถ
- ์์ ์ ์ค์ด๊ธฐ - 128 ๋์
num_colors=48 - ํฌ๊ธฐ ์ค์ด๊ธฐ - 480x480 ๋์ 128x128
- ์ค๋ณต ์ ๊ฑฐ - save()์์
remove_duplicates=True - ์ด๋ชจ์ง ๋ชจ๋ -
optimize_for_emoji=True๊ฐ ์๋ ์ต์ ํ
# ์ด๋ชจ์ง๋ฅผ ์ํ ์ต๋ ์ต์ ํ
builder.save(
'emoji.gif',
num_colors=48,
optimize_for_emoji=True,
remove_duplicates=True
)
์ฒ ํ
์ด ์คํฌ์ด ์ ๊ณตํ๋ ๊ฒ:
- ์ง์: Slack์ ์๊ตฌ์ฌํญ๊ณผ ์ ๋๋ฉ์ด์ ๊ฐ๋
- ์ ํธ๋ฆฌํฐ: GIFBuilder, validators, easing functions
- ์ ์ฐ์ฑ: PIL ๊ธฐ๋ณธ ๋ํ์ ์ฌ์ฉํ์ฌ ์ ๋๋ฉ์ด์ ๋ก์ง ์์ฑ
์ด ์คํฌ์ด ์ ๊ณตํ์ง ์๋ ๊ฒ:
- ๊ณ ์ ๋ ์ ๋๋ฉ์ด์ ํ ํ๋ฆฟ์ด๋ ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง ํจ์
- ์ด๋ชจ์ง ํฐํธ ๋ ๋๋ง (ํ๋ซํผ๋ง๋ค ๋์์ด ๋ค๋ฆ)
- ์คํฌ์ ๋ด์ฅ๋ ๋ฏธ๋ฆฌ ํจํค์ง๋ ๊ทธ๋ํฝ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์ฌ์ฉ์ ์ ๋ก๋์ ๋ํ ์ฐธ๊ณ : ์ด ์คํฌ์ ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง ๊ทธ๋ํฝ์ ํฌํจํ์ง ์์ง๋ง, ์ฌ์ฉ์๊ฐ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ํ๋ฉด PIL์ ์ฌ์ฉํ์ฌ ๋ก๋ํ๊ณ ์์ ํ์ธ์ - ์ฌ์ฉ์์ ์์ฒญ์ ๋ฐ๋ผ ์ง์ ์ฌ์ฉํ ์ง ์๊ฐ์ผ๋ก๋ง ์ฌ์ฉํ ์ง ํ๋จํ์ธ์.
์ฐฝ์์ ์ผ๋ก ๋ง๋์ธ์! ๊ฐ๋ ์ ์กฐํฉํ๊ณ (๋ฐ์ด์ค + ํ์ , ํ์ค + ์ฌ๋ผ์ด๋ ๋ฑ) PIL์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ์ฉํ์ธ์.
์์กด์ฑ
pip install pillow imageio numpy