← 전체 코드 목록

📖 사용 방법

구글 시트에 이미지 설명을 입력하면, 스테이블 디퓨전 AI가 이미지를 만들고 구글 문서에 자동으로 정리해주는 똑똑한 비서 스크립트입니다.

이런 분께 추천해요

  • AI 이미지 생성에 관심 있지만 복잡한 코딩은 어려운 분
  • 반복적인 이미지 생성 및 문서화 작업을 자동화하고 싶은 분
  • 구글 시트와 구글 문서를 자주 활용하는 분
  • 나만의 AI 이미지 아카이브를 만들고 싶은 분

완성하면 이렇게 됩니다

  • 구글 시트의 B열(이미지 설명)에 텍스트를 입력하면, C열에 구글 문서 링크가 자동으로 생성됩니다.
  • 생성된 구글 문서에는 AI가 만든 이미지가 삽입되어 있습니다.
  • A열(제목)이 같으면 같은 문서에 이미지가 추가되고, 다르면 새로운 문서가 만들어집니다.
  • 한글로 프롬프트(이미지 설명)를 입력해도 자동으로 영어로 번역되어 AI에 전달됩니다.

준비물

  • 구글 계정 (구글 시트, 구글 문서, 구글 앱스 스크립트 사용)
  • Stability AI (스테이블리티 AI) 계정 및 API 키

따라하기

1단계: Stability AI API 키 발급받기

Stability AI 웹사이트에 접속하여 회원가입/로그인 후, 'API Keys' 메뉴에서 새로운 API 키를 발급받아 복사해 둡니다. 이 키는 AI가 이미지를 생성할 수 있도록 허락하는 일종의 비밀번호입니다.

2단계: 구글 시트 준비 및 스크립트 붙여넣기

  1. 새 구글 시트를 만드세요. 시트 이름을 'AI 이미지 생성기' 등으로 알아보기 쉽게 변경해 두시면 좋습니다.
  2. 시트의 A1셀에 '제목', B1셀에 '이미지 설명', C1셀에 '문서 링크'를 입력합니다.
  3. 구글 시트 상단 메뉴에서 '확장 프로그램'을 클릭한 후, 'Apps Script'를 선택합니다. 새로운 탭에 스크립트 편집기(Apps Script 에디터)가 열립니다.
  4. 스크립트 편집기에 기본으로 작성되어 있는 코드(function myFunction() { ... })를 모두 지웁니다.
  5. 이 콘텐츠에 제공된 [코드]를 전체 복사하여 스크립트 편집기에 붙여넣습니다.
  6. 붙여넣은 코드 중 상단 '설정 영역' 부분을 찾아 수정합니다.
    • const API_KEY = 'Stability AI API 키 넣는 곳'; 부분에 1단계에서 복사해 둔 Stability AI API 키를 붙여넣으세요. ' ' (작은따옴표) 안에 키를 넣어야 합니다.
    • const SHEET_NAME = '시트1'; 부분에 현재 사용 중인 구글 시트의 정확한 이름을 입력하세요. (예: 'AI 이미지 생성기')
    • const IMAGE_WIDTH = 800;은 문서에 삽입될 이미지의 너비(픽셀 단위)를 설정하는 부분입니다. 원하시는 대로 숫자를 변경할 수 있습니다.
  7. 스크립트 편집기 상단의 저장 버튼(디스크 모양 아이콘)을 클릭하여 스크립트를 저장합니다.

3단계: 트리거 설정하기

이 스크립트는 구글 시트의 B열이 수정될 때 자동으로 실행되도록 설정해야 합니다.

  1. Apps Script 에디터 좌측 메뉴에서 '트리거' 아이콘(시계 모양)을 클릭합니다.
  2. 화면 오른쪽 하단의 '트리거 추가' 버튼을 클릭합니다.
  3. 트리거 설정 창에서 다음 항목들을 선택합니다.
    • 실행할 함수 선택: onEdit
    • 이벤트 소스 선택: 스프레드시트에서
    • 이벤트 유형 선택: 수정 시
  4. '저장' 버튼을 클릭합니다.
  5. 처음 스크립트를 실행할 때 구글 계정 권한 요청 팝업이 나타날 수 있습니다. '내 계정 선택' → '고급' → '안전하지 않은 앱으로 이동' → '허용'을 클릭하여 스크립트가 구글 시트와 문서에 접근할 수 있도록 권한을 부여해 주세요.

4단계: AI 이미지 생성 테스트

  1. 구글 시트로 돌아갑니다.
  2. B열(이미지 설명)에 원하는 이미지 프롬프트(AI에게 만들라고 지시하는 문장)를 입력합니다. 예를 들어 "숲속을 걷는 귀여운 강아지", "미래 도시의 풍경" 등을 입력해 보세요.
  3. A열(제목)에는 해당 이미지들이 들어갈 구글 문서의 제목을 입력합니다. 예를 들어 "강아지 그림 모음" 또는 "미래 도시"라고 입력할 수 있습니다.
  4. B열에 내용을 입력하고 엔터를 누르면, C열에 '처리 중...' 메시지가 나타납니다. 잠시 후 AI가 이미지를 생성하고 구글 문서에 삽입한 뒤, C열에 해당 구글 문서의 링크가 생성됩니다.
  5. 생성된 링크를 클릭하여 AI가 만든 이미지를 확인해 보세요. A열의 제목이 같으면 같은 문서에 이미지가 계속 추가되고, 제목이 다르면 새로운 문서가 만들어집니다.

자주 막히는 부분

  • API 키 오류: 스크립트의 API_KEY 변수에 Stability AI API 키가 정확히 입력되었는지 다시 확인해 주세요. 작은따옴표(' ') 안에 키가 들어가야 합니다.
  • 트리거 설정 오류: Apps Script 에디터의 '트리거' 메뉴에서 onEdit 함수가 '수정 시' 이벤트로 올바르게 설정되었는지 확인해 주세요.
  • 권한 허용 문제: 스크립트 실행 시 구글 계정 권한을 '허용'했는지 다시 확인해 보세요. 권한이 없으면 구글 문서에 이미지를 삽입할 수 없습니다.
  • 시트 이름 불일치: 스크립트의 SHEET_NAME 변수에 입력한 시트 이름이 실제 구글 시트의 이름과 띄어쓰기, 대소문자까지 정확히 일치하는지 확인하세요.
  • B열 외 다른 열 수정: 이 스크립트는 B열(이미지 설명)이 수정될 때만 작동합니다. 다른 열을 수정해도 AI 이미지가 생성되지 않습니다.

이렇게도 써보세요 (응용)

  • 다양한 이미지 생성: B열에 여러 프롬프트를 입력하여 다양한 이미지를 한 번에 생성하고, A열 제목을 같게 하여 하나의 문서에 모아보세요.
  • 아이디어 스케치: 제품 디자인, 캐릭터 구상 등 초기 아이디어 단계에서 빠르게 시각 자료를 만들 때 활용해 보세요.
  • 콘텐츠 제작 보조: 블로그 글, 발표 자료 등에 필요한 이미지를 효율적으로 생성하고 관리할 수 있습니다.
  • 이미지 너비 조절: 스크립트 상단의 IMAGE_WIDTH 값을 변경하여 문서에 삽입될 이미지의 너비를 조절할 수 있습니다. (높이는 비율에 맞게 자동으로 조절됩니다.)

한 줄 정리

구글 시트에 프롬프트만 입력하면 AI 이미지가 자동으로 생성되어 구글 문서에 착착 정리되는 나만의 AI 이미지 비서를 만들어 보세요!

💻 코드

Code.gs
/**
 * 구글 시트와 스테이블 디퓨전 API를 연동하여 문서를 생성하는 스크립트
 * * 기능:
 * 1. 시트의 B열(프롬프트)에 내용이 입력되면 자동으로 실행됩니다 (onEdit 트리거).
 * 2. 한글 프롬프트는 자동으로 영어로 번역되어 API에 전달됩니다.
 * 3. A열의 제목이 연속되면 같은 문서에, 다르면 새 문서에 이미지를 추가합니다.
 * 4. 생성된 문서 링크를 C열에 기록합니다.
 */

// ==========================================
// [설정 영역] 사용자의 API 정보를 입력하세요
// ==========================================
// 예시: Stability AI (DreamStudio) API 키
const API_KEY = 'Stability AI API 키 넣는 곳'; 

// 최신 SDXL 1.0 정식 버전 엔드포인트 (1024x1024 지원)
const API_URL = 'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image';

// 작업할 시트 이름 (없으면 첫 번째 시트를 자동으로 사용합니다)
const SHEET_NAME = '시트1'; 

// [요청 반영] 문서에 삽입할 이미지 너비 (높이는 비율에 맞게 자동 조절)
const IMAGE_WIDTH = 800; 

// ==========================================
// [트리거 함수] B열 수정 시 자동 실행되도록 설정
// ==========================================

/**
 * 시트가 수정될 때 자동으로 실행되는 함수입니다.
 * B열에 내용이 입력되면 generateImageForSingleRow 함수를 호출합니다.
 * 이 함수를 사용하려면 Apps Script 에디터에서 트리거를 설정해야 합니다.
 * 설정 방법은 아래 설명을 참고해주세요.
 */
function onEdit(e) {
  // 이벤트 객체에서 수정된 셀 정보 추출
  const range = e.range;
  const sheet = range.getSheet();
  const sheetName = sheet.getName();
  
  // 1. 설정된 시트 이름과 일치하는지 확인
  // 2. 수정된 열이 B열(2번째 열)인지 확인
  // 3. 행이 데이터 영역(2행 이상)인지 확인
  if (sheetName === SHEET_NAME && range.getColumn() === 2 && range.getRow() > 1) {
    // B열이 수정되면 해당 행만 처리하는 함수를 호출
    generateImageForSingleRow(sheet, range.getRow());
  }
}

// ==========================================
// [메인 함수] 단일 행을 처리하는 함수
// ==========================================
function generateImageForSingleRow(sheet, rowIndex) {
  const rowData = sheet.getRange(rowIndex, 1, 1, 3).getValues()[0];
  
  const bookTitle = rowData[0]; // A열: 책 제목
  const prompt = rowData[1];    // B열: 이미지 설명
  const existingLink = rowData[2]; // C열: 기존 링크
  
  // 제목이나 프롬프트가 없으면 중단
  if (!bookTitle || !prompt) return;

  // 이미 링크가 생성되어 있고(성공 케이스), 프롬프트가 변경되지 않은 경우 중복 방지
  if (existingLink && !existingLink.toString().startsWith("실패") && !existingLink.toString().startsWith("에러")) {
    // 이미 처리가 완료된 경우 스킵할 수 있으나, onEdit은 프롬프트 변경 시 항상 실행됨.
    // 여기서는 프롬프트 변경 시 재작업을 허용함.
  }
    
  Logger.log(`처리 중(행 ${rowIndex}): ${bookTitle}`);
  sheet.getRange(rowIndex, 3).setValue("처리 중..."); 
  SpreadsheetApp.flush(); 
  
  // 1. 번역 로직
  let translatedPrompt = prompt;
  try {
    if (/[가-힣]/.test(prompt)) { 
      translatedPrompt = LanguageApp.translate(prompt, 'ko', 'en');
      Logger.log(`번역됨: "${prompt}" -> "${translatedPrompt}"`);
    }
  } catch (e) {
    Logger.log(`번역 실패, 원본 사용: ${e.toString()}`);
    translatedPrompt = prompt;
  }

  try {
    // 2. 이미지 생성 API 호출 (번역된 프롬프트 사용)
    const apiResult = callStableDiffusionAPI(translatedPrompt);
    
    if (!apiResult.success) {
      const errorMsg = `실패: ${apiResult.error}`;
      Logger.log(errorMsg);
      sheet.getRange(rowIndex, 3).setValue(errorMsg);
      return;
    }

    const imageBlob = apiResult.blob;

    // 3. 문서 ID 및 URL 결정 (이전 행의 데이터 가져오기)
    let doc;
    let body;
    let currentDocUrl;
    let currentDocId;
    
    // 이전 행의 제목과 비교 (i는 1부터 시작하므로 rowIndex - 1)
    const prevRowIndex = rowIndex - 1;
    const prevRowData = prevRowIndex > 1 ? sheet.getRange(prevRowIndex, 1, 1, 3).getValues()[0] : null;
    const previousTitle = prevRowData ? prevRowData[0] : "";
    const prevDocLink = prevRowData ? prevRowData[2] : "";

    // 이전 행의 제목과 같고, 이전 행에 유효한 문서 링크가 있는 경우
    if (bookTitle === previousTitle && prevDocLink && !prevDocLink.toString().startsWith("실패")) {
      try {
        currentDocId = DocumentApp.openByUrl(prevDocLink).getId();
        currentDocUrl = prevDocLink;
        doc = DocumentApp.openById(currentDocId);
        body = doc.getBody();
      } catch(e) {
         // 문서 열기 실패 시 새 문서 생성
         doc = DocumentApp.create(bookTitle + " - 이미지 모음");
         Utilities.sleep(500); // 안전장치
         currentDocId = doc.getId();
         currentDocUrl = doc.getUrl();
         body = doc.getBody();
         
         try {
           const titlePara = body.insertParagraph(0, bookTitle);
           titlePara.setHeading(DocumentApp.ParagraphHeading.HEADING_1);
         } catch (styleError) {
           Logger.log(`제목 스타일 적용 실패(내용은 유지): ${styleError}`);
         }
      }
    } else {
      // 제목이 다르거나 첫 시작이면 새 문서 생성
      doc = DocumentApp.create(bookTitle + " - 이미지 모음");
      Utilities.sleep(500); // 안전장치
      currentDocId = doc.getId();
      currentDocUrl = doc.getUrl();
      body = doc.getBody();
      
      try {
        const titlePara = body.insertParagraph(0, bookTitle);
        titlePara.setHeading(DocumentApp.ParagraphHeading.HEADING_1);
      } catch (styleError) {
        Logger.log(`제목 스타일 적용 실패(내용은 유지): ${styleError}`);
      }
    }
    
    // 4. 문서에 이미지 및 설명 추가
    // [요청 반영] 프롬프트 텍스트 제거하고 이미지와 구분선만 추가
    try {
      // 이미지 삽입
      body.appendImage(imageBlob)
          .setWidth(IMAGE_WIDTH); 
    } catch (imgError) {
      sheet.getRange(rowIndex, 3).setValue("실패: 이미지 포맷 오류");
      return;
    }
    
    // 문서 구분을 위한 내용 추가 (선택 사항)
    body.appendParagraph("--------------------------------------------------");
    
    doc.saveAndClose(); 
    
    // 5. 시트 C열에 링크 기록
    sheet.getRange(rowIndex, 3).setValue(currentDocUrl);
    
  } catch (e) {
    Logger.log(`에러 발생 (행 ${rowIndex}): ${e.toString()}`);
    sheet.getRange(rowIndex, 3).setValue(`에러: ${e.toString()}`);
  }
}

// ==========================================
// [API 호출 함수] 스테이블 디퓨전 API 통신
// ==========================================
function callStableDiffusionAPI(prompt) {
  const payload = {
    "text_prompts": [
      {
        "text": prompt,
        "weight": 1
      }
    ],
    "cfg_scale": 7,
    "height": 1024,
    "width": 1024,
    "samples": 1,
    "steps": 30
  };

  const options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Accept": "image/png",
      "Authorization": `Bearer ${API_KEY}`
    },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  try {
    const response = UrlFetchApp.fetch(API_URL, options);
    const responseCode = response.getResponseCode();
    
    if (responseCode === 200) {
      return { success: true, blob: response.getBlob() };
    } else {
      const errorText = response.getContentText();
      Logger.log(`API 오류(${responseCode}): ${errorText}`);
      try {
        const errorJson = JSON.parse(errorText);
        return { success: false, error: errorJson.message || `API 오류(${responseCode})` };
      } catch (e) {
        return { success: false, error: `API 오류(${responseCode})` };
      }
    }
  } catch (e) {
    Logger.log(`API 통신 실패: ${e}`);
    return { success: false, error: "통신 실패(인터넷/URL 확인)" };
  }
}

🎬 관련 영상