소개
요즘 번역을 취합해서 JSON으로 만들어야 하는데 생각보다 시간이 걸립니다. 😅 스프레드시트로 히스토리를 관리할 수는 있었지만 트래킹이 쉽지 않고 , 전체 셀 업데이트가 이뤄지면 제가 업데이트해둔 내용이 예전 버전으로 돌아가 버리는 일도 있고, 변경 고지를 못받고 나중에 수정하다 알게 되기도 했어요. 게다가 협업자마다 사용하는 툴도 달랐습니다. (저는 구글 시트, 상대방은 JSON 혹은 엑셀) 이 간극을 좁히지 않으면 계속 비효율이 쌓일 것 같았습니다.
처음엔 커서를 믿고 다 붙여넣어서 정리시키니 잘 되는 것 같았는데, 이상하게 수치를 바꿔서 수정하거나 갑자기 다국어가 섞인 JSON을 만들어줘서 밤을 꼴딱 새운 적도 있었어요. 이런 시행착오를 겪으면서 더더욱 자동화의 필요성을 절실히 느끼게 되었습니다.
그래서 “이 잡무를 자동화해야겠다!“는 결심을 하게 되었습니다. 💡
진행 방법
업무로 직접 진행하기는 어려우니 샘플로…
시트 스키마 제안
시트 이름: translations
헤더(1행): key | ko | en | note | updated_at
규칙: 키(key)는 중복 금지, 값은 빈 칸 허용. 수정 시 updated_at은 스크립트가 자동 갱신.
Apps Script 설정
시트 → 확장 프로그램 → 앱 스크립트를 열고 아래 코드를 붙여넣습니다.
프로젝트 설정 → 스크립트 속성에 다음 키를 추가합니다.
SLACK_WEBHOOK : 슬랙 인커밍 웹훅 URL
JSON_FOLDER_ID : JSON을 저장할 드라이브 폴더 ID(옵션)
메뉴 실행 → generateJson을 최초 1회 실행하여 권한을 부여합니다.
코드: JSON 생성 + Slack 알림 + 변경 감지(onEdit)
/** CONFIG **/
const PROP = PropertiesService.getScriptProperties();
const SLACK_WEBHOOK = PROP.getProperty('SLACK_WEBHOOK');
const JSON_FOLDER_ID = PROP.getProperty('JSON_FOLDER_ID');
/** 메인: 시트 → JSON 생성 및 드라이브 저장 */
function generateJson() {
const sheet = SpreadsheetApp.getActive().getSheetByName('translations');
const data = sheet.getDataRange().getValues();
const header = data.shift();
const idx = Object.fromEntries(header.map((h, i) => [String(h).trim(), i]));
const obj = {};
data.forEach(row => {
const key = row[idx.key];
if (!key) return;
obj[key] = {
ko: row[idx.ko] ?? '',
en: row[idx.en] ?? '',
note: row[idx.note] ?? ''
};
});
const jsonText = JSON.stringify(obj, null, 2);
const fileId = saveJson(jsonText);
postToSlack({
text: `✅ JSON 생성 완료 — ${Object.keys(obj).length}개 키`,
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: `*JSON 생성 완료* (키 ${Object.keys(obj).length}개)` } },
{ type: 'context', elements: [ { type: 'mrkdwn', text: `파일 ID: ${fileId}` } ] }
]
});
return jsonText;
}
/** 드라이브에 JSON 저장 */
function saveJson(jsonText) {
const tz = Session.getScriptTimeZone();
const name = `translations_${Utilities.formatDate(new Date(), tz, 'yyyyMMdd_HHmmss')}.json`;
let file;
if (JSON_FOLDER_ID) {
const folder = DriveApp.getFolderById(JSON_FOLDER_ID);
file = folder.createFile(name, jsonText, MimeType.JSON);
} else {
file = DriveApp.createFile(name, jsonText, MimeType.JSON);
}
return file.getId();
}
/** 셀 수정 시 변경 감지 및 슬랙 알림 */
function onEdit(e) {
try {
if (!e) return;
const sheet = e.source.getActiveSheet();
if (sheet.getName() !== 'translations') return; // 대상 시트만 감시
// 단일 셀 편집일 때만 oldValue 제공됨
const row = e.range.getRow();
const col = e.range.getColumn();
if (row === 1) return; // 헤더 제외
const key = sheet.getRange(row, 1).getValue();
const header = sheet.getRange(1, col).getValue();
const newVal = e.value;
const oldVal = e.oldValue; // 단일 셀 수정 시에만 존재
// updated_at 자동 기록
const updatedAtCol = sheet.getRange(1, 1).getDataRegion(SpreadsheetApp.Dimension.COLUMNS).getLastColumn();
const updatedAtHeader = sheet.getRange(1, updatedAtCol).getValue();
if (String(updatedAtHeader).toLowerCase() === 'updated_at') {
sheet.getRange(row, updatedAtCol).setValue(new Date());
}
postToSlack({
text: `✏️ 변경: ${key}`,
blocks: [
{ type: 'section', text: { type: 'mrkdwn', text: `*${sheet.getName()}* — R${row}C${col} • *${key}*` } },
{ type: 'section', fields: [
{ type: 'mrkdwn', text: `*컬럼*
${header}` },
{ type: 'mrkdwn', text: `*셀*
R${row}C${col}` }
] },
{ type: 'section', fields: [
{ type: 'mrkdwn', text: `*이전*
\`${oldVal ?? '(없음)'}\`` },
{ type: 'mrkdwn', text: `*변경*
\`${newVal ?? '(삭제)'}\`` }
] }
]
});
} catch (err) {
postToSlack({ text: `⚠️ onEdit 오류: ${err.message}` });
}
}
/** Slack 웹훅으로 메시지 전송 */
function postToSlack(payload) {
if (!SLACK_WEBHOOK) throw new Error('SLACK_WEBHOOK 스크립트 속성을 설정하세요.');
const res = UrlFetchApp.fetch(SLACK_WEBHOOK, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
return res.getResponseCode();
}트리거 구성
수정 알림: onEdit(e)는 간단 트리거로 자동 동작합니다. 단, 단일 셀 편집일 때만 e.oldValue가 제공됩니다.
정기 JSON 생성(옵션): generateJson에 시간 기반 트리거를 설치하면 매일/매시간 자동으로 JSON을 생성하고 히스토리를 남길 수 있습니다.
Slack 인커밍 웹훅 설정 요약
워크스페이스에서 Incoming Webhooks 앱 추가
채널을 선택해 Webhook URL 발급
발급받은 URL을 스크립트 속성 SLACK_WEBHOOK에 저장
메시지 포맷은 위 코드의 blocks처럼 마크다운과 블록 레이아웃을 활용하면 가독성이 좋아집니다.
이 정도까지 작업을 해보고 적용은 아직 전이니
이런저런 테스트를 차주에 진행해보려 합니다 ㅎ
지피티와 이야기하다보니 아래 고급화 아이디어까지?
(고급) 다중 셀 변경 추적용 버퍼/감사 로그 패턴
(고급) 매일 18:00 요약 알림 타임 트리거
(고급) JSON 스키마(i18n 네임스페이스) & 검증기
권장 워크플로우
ensureSetup() 한 번 실행해 버퍼/로그 시트를 준비
평소 편집은 translations에서 수행 → onEdit가 변경을 로깅하고 버퍼를 최신화
일일 요약은 시간 트리거로 sendDailySummary 실행
JSON 내보내기는 generateJsonValidated로 실행하여 스키마 검증 후 생성
이렇게 워크플로우까지 추천해줘서 앞으로 이 내용으로 적용해보고 검토해보고 또 개선하며 최적으로 자동화해보려고 합니다. 서버 업데이트까진 어려울 거 같은데 가능할지 고민해봐야겠어요.
화이팅!!
결과와 배운 점
깨달음
자동화는 늘 하고 싶지만 뭐 해야하지 하고 늘 고민했던 부분이어서 막막했었는데. 해야할 게 생각해보니 있어서 기뻤어요.
툴이 다양한데 뭘 쓸 수 있었나 싶었는데 이미 있는 툴을 그냥 쓰는 게 아니라, “내 업무에 맞게 최적화”하는 과정이라는 걸 알게 되었습니다. 🙌
앞으로의 계획:
Cursor와 Slack 같은 도구를 활용해서 수정 사항을 빠르게 알림받고, 소중한 시간을 허투루 쓰지 않고 정리까지 깔끔하게 하는 걸 목표로 하고 있습니다.