Ideveloper's
Thinking

ideveloper
Front end Developer who steadily study
Jul 20, 2025 - 4 min read

๋””์ž์ด๋„ˆ๋ฅผ ๋•๋Š” ํ”ผ๊ทธ๋งˆ(figma)ํ”Œ๋Ÿฌ๊ทธ์ธ ๋งŒ๋“ค๊ธฐ

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ํ˜‘์—… ๊ณผ์ •์—์„œ ๋ฐ˜๋ณต์ ์ด๊ณ  ๋น„ํšจ์œจ์ ์ธ ์ž‘์—…๋“ค์ด ์กด์žฌํ• ์ˆ˜ ์žˆ๋Š”๋ฐ์š”.

๋˜ํ•œ ํŠนํžˆ ์Šคํƒ€ํŠธ์—…์ฒ˜๋Ÿผ ๋ฆฌ์†Œ์Šค๊ฐ€ ํ•œ์ •๋œ ํ™˜๊ฒฝ์—์„œ๋Š” "์–ด๋–ป๊ฒŒ ๋” ์ž˜ ๋งŒ๋“ค๊นŒ?" ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ "์–ด๋–ป๊ฒŒ ๋” ์ž˜ ์ผํ•  ์ˆ˜ ์žˆ์„๊นŒ?" ๋„ ์ค‘์š”ํ•œ ๊ณ ๋ฏผ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋ฌธ์ œ์˜์‹์—์„œ ์ถœ๋ฐœํ•ด, ์ด ๊ธ€์—์„œ ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•๋“ค์„ ์‹œ๋„ํ•ด๋ณด๊ธฐ๋„ ํ–ˆ๋Š”๋ฐ์š”.

์ด๋ฒˆ์—” ์กฐ๊ธˆ ๋” ๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—…์— ์ง‘์ค‘ํ•ด์„œ ํ˜‘์—…๊ณผ์ •์—์„œ์˜ ๋น„ํšจ์œจ/๋ณ‘๋ชฉ์„ ์ค„์ด๊ธฐ ์œ„ํ•œ Figma ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋งŒ๋“ค์—ˆ๊ณ  ๊ทธ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๐ŸŽฏ ๋ฌธ์ œ ์ •์˜

๋””์ž์ด๋„ˆ-๊ฐœ๋ฐœ์ž ํ˜‘์—…์˜ ๋ณ‘๋ชฉ ์ง€์ 

์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ ํ”„๋กœ์„ธ์Šค

  • ์•„์ด์ฝ˜์ด ์ƒˆ๋กœ ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๋””์ž์ธ ์‹œ์Šคํ…œ์— SVG ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€
  • ๋น„์Šทํ•ด ๋ณด์ด๋Š” ์•„์ด์ฝ˜์„ ์ž˜๋ชป ์‚ฌ์šฉํ•˜์—ฌ QA ๊ณผ์ •์—์„œ ๋’ค๋Šฆ๊ฒŒ ์ถ”๊ฐ€ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๋Š” ์ผ์ด ๋นˆ๋ฒˆ

๋””์ž์ด๋„ˆ์˜ ๋ฐ˜๋ณต ์ž‘์—…

  • ๋น ๋“ฏํ•œ ์ผ์ • ์†์—์„œ ๋””์ž์ธ ์‹œ์•ˆ์— ์˜คํƒ€๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒ
  • ๋””์ž์ด๋„ˆ๊ฐ€ ๋งค๋ฒˆ ChatGPT์—๊ฒŒ UX writing ์›์น™์„ ๋ฌผ์–ด๋ณด๋ฉฐ ์ˆ˜์ž‘์—…์œผ๋กœ ์ˆ˜์ •
  • Figma โ†” ChatGPT๋ฅผ ์˜ค๊ฐ€๋Š” ๋ฃจํ‹ด์ด ์ง€์†์ ์œผ๋กœ ๋ฐ˜๋ณต

๐Ÿ› ๏ธ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

1. ์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ ์ž๋™ ์ƒ์„ฑ ํ”Œ๋Ÿฌ๊ทธ์ธ

๐Ÿ“‹ ๊ธฐ๋Šฅ ๊ฐœ์š”

๋””์ž์ด๋„ˆ๊ฐ€ Figma์—์„œ SVG ์•„์ด์ฝ˜์„ ์„ ํƒํ•ด GitHub์— ์ž๋™์œผ๋กœ PR์„ ์ƒ์„ฑํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ

๐Ÿ”„ ์›Œํฌํ”Œ๋กœ์šฐ

  1. Figma์—์„œ SVG ์•„์ด์ฝ˜ ์„ ํƒ
  2. ๋‚ด๋ถ€ ๋””์ž์ธ ์‹œ์Šคํ…œ ํฌ๋งท(React SVG ์ปดํฌ๋„ŒํŠธ)์œผ๋กœ ๋ณ€ํ™˜
  3. GitHub API๋ฅผ ํ†ตํ•ด ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ, ํŒŒ์ผ ์—…๋กœ๋“œ, PR ์ƒ์„ฑ

ํ•ต์‹ฌ ๊ตฌํ˜„ ๋‚ด์šฉ

SVG to React Component ๋ณ€ํ™˜

// SVG Export ๋ฐ ์ •๋ฆฌ
const svgBytes = await exportNode.exportAsync({
  format: 'SVG',
  svgOutlineText: true,
  svgIdAttribute: false,
  svgSimplifyStroke: true,
});

// ๋ถˆํ•„์š”ํ•œ ์š”์†Œ ์ œ๊ฑฐ
const cleanSvgString = svgString
  .replace(/<g[^>]*>/g, '')
  .replace(/<\/g>/g, '')
  .replace(/<defs>[\s\S]*?<\/defs>/g, '')
  .replace(/clip-path="[^"]*"/g, '');

// React ์ปดํฌ๋„ŒํŠธ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ
const reactComponent = `
import { Colors } from '../../colors';
import { Svg } from '../../utils/IconBase';
import { IconProps } from '../../utils/icon';

const ${componentName} = ({ width = 24, height = 24, fill = Colors.gray900, ...props }: IconProps) => {
  return (
    <Svg width={width} height={height} viewBox="${viewBox}" fill="none" {...props}>
      <path d="${pathData}" fill={fill} />
    </Svg>
  );
};

export default ${componentName};
`;

GitHub API๋ฅผ ํ†ตํ•œ ์ž๋™ PR ์ƒ์„ฑ

// 1. ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ
const createBranchRes = await fetch(
  `https://api.github.com/repos/${owner}/${repo}/git/refs`,
  {
    method: 'POST',
    headers: {
      Authorization: `token ${githubToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha: baseSha }),
  }
);

// 2. ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ์ปค๋ฐ‹
const treeRes = await fetch(
  `https://api.github.com/repos/${owner}/${repo}/git/trees`,
  {
    method: 'POST',
    body: JSON.stringify({
      base_tree: baseSha,
      tree: [
        {
          path: componentPath,
          mode: '100644',
          type: 'blob',
          sha: compBlob.sha,
        },
        { path: indexPath, mode: '100644', type: 'blob', sha: idxBlob.sha },
      ],
    }),
  }
);

// 3. PR ์ƒ์„ฑ
const prRes = await fetch(
  `https://api.github.com/repos/${owner}/${repo}/pulls`,
  {
    method: 'POST',
    body: JSON.stringify({
      title: `[๐Ÿค–] feat: add icon component ${pascalName}`,
      head: branchName,
      base: baseBranch,
      body: `Add new icon component: ${pascalName}`,
    }),
  }
);

ํ”ผ๊ทธ๋งˆ์— ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ

function notifyUI(msg: string) {
  figma.ui.postMessage({ type: 'github-upload-status', message: msg });
}

// ์ง„ํ–‰ ๋‹จ๊ณ„๋ณ„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
const steps = [
  '๋ธŒ๋žœ์น˜ ์ •๋ณด ์กฐํšŒ ์ค‘...',
  '์ƒˆ ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ ์ค‘...',
  'index.tsx ์ฝ๊ธฐ ์ค‘...',
  'tree ์ƒ์„ฑ ์ค‘...',
  '์ปค๋ฐ‹ ์ƒ์„ฑ ์ค‘...',
  '๋ธŒ๋žœ์น˜(ref) ์—…๋ฐ์ดํŠธ ์ค‘...',
];

steps.forEach((step, index) => {
  notifyUI(`${index + 1}/${steps.length}: ${step}`);
});

2. UX Writing/์˜คํƒ€ ๊ฒ€์ฆ ํ”Œ๋Ÿฌ๊ทธ์ธ

๐Ÿ“‹ ๊ธฐ๋Šฅ ๊ฐœ์š”

์„ ํƒํ•œ ํ…์ŠคํŠธ๋ฅผ OpenAI API๋ฅผ ํ™œ์šฉํ•ด ๊ฒ€ํ† ํ•˜์—ฌ UX writing ๊ฐœ์„ ์‚ฌํ•ญ์„ ์ œ์•ˆํ•˜๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ

๐Ÿ”„ ์›Œํฌํ”Œ๋กœ์šฐ

  1. Figma์—์„œ ํ”„๋ ˆ์ž„ ์„ ํƒ โ†’ ํ…์ŠคํŠธ ๋…ธ๋“œ ์ž๋™ ์ถ”์ถœ
  2. ๊ฒ€์‚ฌ ํ•ญ๋ชฉ(๋งž์ถค๋ฒ•, UX writing ๋“ฑ) ์„ ํƒ
  3. ์ปค์Šคํ…€ ํ”„๋กฌํ”„ํŠธ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)
  4. OpenAI API๋ฅผ ํ†ตํ•œ ๊ฒ€ํ†  ๊ฒฐ๊ณผ ํ‘œ์‹œ

ํ•ต์‹ฌ ๊ตฌํ˜„ ๋‚ด์šฉ

(Figma)ํ…์ŠคํŠธ ๋…ธ๋“œ ์ถ”์ถœ

function getAllTextNodes(node: SceneNode | PageNode): TextNode[] {
  let result: TextNode[] = [];

  // ์ˆจ๊ฒจ์ง„ ์š”์†Œ ์ œ์™ธ
  if ('visible' in node && node.visible === false) return result;

  if (node.type === 'TEXT') {
    result.push(node);
  } else if ('children' in node) {
    for (const child of node.children) {
      result = result.concat(getAllTextNodes(child));
    }
  }

  return result;
}

(Figma)ํ…์ŠคํŠธ ๊ฒ€์ฆ์„ ์œ„ํ•ด API์— ์š”์ฒญ

// UI์—์„œ ๊ฒ€์ฆ ์š”์ฒญ (ui.tsx)
async function checkSpelling(text: string, promptString?: string): Promise<SpellCheckResponse> {
  return new Promise((resolve) => {

    function handler(event: MessageEvent) {
      const msg = event.data.pluginMessage;
      if (msg?.type === 'SPELL_CHECK_RESULT') {
        window.removeEventListener('message', handler);
        resolve(msg.result);
      }
    }

    window.addEventListener('message', handler);
    parent.postMessage({
      pluginMessage: { type: 'PROXY_SPELL_CHECK', text, promptString }
    }, '*');
  });
}

// API ํ˜ธ์ถœ (code.ts)
  if (msg.type === 'PROXY_SPELL_CHECK') {
    const { text, promptString } = msg;

    try {
      const res = await fetch(
        API_URL,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-cors-token': process.env.REACT_APP_CORS_TOKEN,
          },
          body: JSON.stringify({
            text,
            prompt: promptString || '',
          }),
        }
      );

      const result = await res.json();
      figma.ui.postMessage({ type: 'SPELL_CHECK_RESULT', result });
    } catch (e) {
      figma.ui.postMessage({
        type: 'SPELL_CHECK_RESULT',
        result: { error: true, message: e?.message || String(e) },
      });
    }
  }

(API)๋ณด์•ˆ ๋ฐ ์ ‘๊ทผ ์ œ์–ด

// Figma ์ „์šฉ ์ ‘๊ทผ ์ œํ•œ
function isFigmaRequest(req: NextApiRequest): boolean {
  const userAgent = req.headers['user-agent'] || '';
  return userAgent.includes('Figma');
}

// ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ
const allowedToken = process.env.FIGMA_CORS_TOKEN;
const requestToken =
  req.headers['x-cors-token'] || req.headers['authorization'];

if (!requestToken || requestToken !== allowedToken) {
  return res.status(401).json({
    error: {
      message: 'Unauthorized. Valid token required.',
      type: 'unauthorized',
    },
  });
}

(API) CORS ์ œ์–ด

[์ฐธ๊ณ ์‚ฌํ•ญ]

  • figma ํ”Œ๋Ÿฌ๊ทธ์ธ์—์„œ ์š”์ฒญ์„ ํ• ๋•Œ๋Š” origin์ด null๋กœ ์™€์„œ ๊ด€๋ จํ•œ CORS์„ธํŒ…์ด ํ•„์ˆ˜๋‹ค (figma๋Š” iframe ๊ธฐ๋ฐ˜)
  • origin์ด null๋กœ ์˜ค๊ธฐ๋•Œ๋ฌธ์— Access-Control-Allow-Origin์— ๋„๋ฉ”์ธ ์„ค์ •์ด ํž˜๋“œ๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ„๋„ token์„ ํ†ตํ•ด ์ œ์–ดํ–ˆ๋‹ค.
function setCorsHeaders(req: NextApiRequest, res: NextApiResponse) {
  const requestToken =
    req.headers['x-cors-token'] || req.headers['authorization'];

  // ํ† ํฐ์ด ์ผ์น˜ํ•  ๋•Œ๋งŒ CORS ํ—ˆ์šฉ
  if (
    req.method === 'OPTIONS' ||
    (requestToken && allowedToken && requestToken === allowedToken)
  ) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader(
      'Access-Control-Allow-Methods',
      'GET, POST, PUT, DELETE, OPTIONS'
    );
    res.setHeader(
      'Access-Control-Allow-Headers',
      'Content-Type, Authorization, x-cors-token'
    );
  }
}

(API)OpenAI API ํ†ตํ•ด ๊ฒ€์ฆ

async function checkTexts(texts: string, customPrompt: string | undefined, openaiApiKey: string): Promise<string> {
  const defaultPrompt = `๋‹ค์Œ ํ•œ๊ตญ์–ด ํ…์ŠคํŠธ๋“ค์˜ ๋งž์ถค๋ฒ•๊ณผ ๋ฌธ๋ฒ•์„ ๊ฒ€ํ† ํ•˜๊ณ  ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”.`;
  const prompt = customPrompt || defaultPrompt;

  const openaiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${openaiApiKey}`,
    },
    body: JSON.stringify({
      model: 'gpt-3.5-turbo',
      messages: [
        { role: 'system', content: prompt },
        { role: 'user', content: `๋‹ค์Œ ๋ฌธ์žฅ๋“ค์„ ๋‹ค์‹œ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”:\n${texts}` },
      ],
      temperature: 0.1, // ์ผ๊ด€์„ฑ์„ ์œ„ํ•œ ๋‚ฎ์€ ์˜จ๋„ ์„ค์ •
      max_tokens: 4096,
    }),
  });

  const openaiData = await openaiResponse.json();
  return openaiData.choices[0]?.message?.content;
}

๐Ÿ“Š ๊ฒฐ๊ณผ ๋ฐ ํšจ๊ณผ

ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ ํ™”๋ฉด

1.์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ PR์ƒ์„ฑ

์•„์ด์ฝ˜ ์ž๋™์ƒ์„ฑa ์•„์ด์ฝ˜ ์ž๋™์ƒ์„ฑb

2.์˜คํƒ€/UX writing ๊ฒ€์ฆ ์˜คํƒ€/UX writing ๊ฒ€์ฆ ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ ํ™”๋ฉดa ์˜คํƒ€/UX writing ๊ฒ€์ฆ ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ ํ™”๋ฉดb

์—…๋ฌด ํšจ์œจ์„ฑ ๊ฐœ์„ 

์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ ์ƒ์„ฑ ์‹œ๊ฐ„ ๋‹จ์ถ• ( ๋””์ž์ด๋„ˆ์˜ ๋””์ž์ธ์‹œ์Šคํ…œ๊ธฐ์—ฌ์™€ ๊ฐœ๋ฐœ์ž ์—…๋ฌด ๋ณ‘๋ ฌ๋กœ ์ง„ํ–‰ ๊ฐ€๋Šฅ)

  • ๊ธฐ์กด: ๋””์ž์ด๋„ˆ ์ถ”๊ฐ€ ์š”์ฒญ or ๋””์ž์ธ QA์ค‘ ์ถ”๊ฐ€ ํ•„์š”ํ•œ ์•„์ด์ฝ˜ ๋ฐœ๊ฒฌ โ†’ ๊ฐœ๋ฐœ์ž ์ž‘์—… โ†’ ์ฝ”๋“œ ๋ฆฌ๋ทฐ โ†’ ๋ฐฐํฌ -> ์ถ”๊ฐ€ ์ž‘์—…
  • ๊ฐœ์„ : ๋””์ž์ด๋„ˆ ์ง์ ‘ ์ƒ์„ฑ โ†’ ์ž๋™ PR โ†’ ์ฝ”๋“œ ๋ฆฌ๋ทฐ โ†’ ๋ฐฐํฌ -> ์ถ”ํ›„ ๊ฐœ๋ฐœ์ž๊ฐ€ ํ™œ์šฉ

ํ…์ŠคํŠธ ๊ฒ€ํ†  ํ”„๋กœ์„ธ์Šค ๊ฐ„์†Œํ™” ( ๋””์ž์ด๋„ˆ์˜ ์ž‘์—… ํšจ์œจ์„ฑ ํ–ฅ์ƒ)

  • ๊ธฐ์กด: Figma โ†’ ChatGPT โ†’ ์ˆ˜๋™ ๋ณต์‚ฌ โ†’ Figma ์ˆ˜์ • (๋ฐ˜๋ณต)
  • ๊ฐœ์„ : ๊ธฐ์กด ๋ฐ˜๋ณต์ ์œผ๋กœ GPT<-> ํ”ผ๊ทธ๋งˆ ์„œ๋น„์Šค๋ฅผ ์˜ค๊ฐ€๋ฉฐ ์ž‘์—…ํ•˜๋Š” ๋ฃจํ‹ด์„ ์ค„์ž„

Powered with by Ideveloper