Build in Public Ep.7: 비개발자 세일즈맨이 Apex와 LWC로 Slack Connect <>Salesforce 실시간 연동을 직접 만든 이야기
들어가며: 링크만으로는 부족했다
지난 글(Ep.6)에서 Salesforce에 "Slack Connect 채널을 Account 페이지에서 볼 수 있냐"고 물었더니 "안 됩니다"라는 답변을 받았다는 글을 썼습니다. 그리고 Claude와 함께 반나절 만에 커스텀 LWC를 만들어서 Slack Connect 채널 링크를 Account 페이지에 표시하는 데까지 성공했거든요.
그런데 며칠 쓰다 보니 한계가 느껴졌습니다. 링크를 클릭하면 Slack으로 넘어가야 하고, 거기서 대화를 읽고, 다시 Salesforce로 돌아와야 합니다. 이러면 결국 컨텍스트 스위칭이 발생하는 거예요. CRM이 Single Source of Truth라면, Salesforce를 떠나지 않고도 고객과의 최근 대화를 바로 확인할 수 있어야 합니다.
이번 글에서는 그 다음 단계를 다룹니다. Slack Connect 채널의 실제 메시지를 Salesforce Account 페이지 안에서 직접 읽을 수 있게 만들었어요. Apex 한 줄 써본 적 없는 세일즈 직군이 Claude와 함께 6개 Apex 클래스와 LWC를 Production에 배포하기까지, 삽질 포함해서 전부요.
전체 아키텍처: 한 장으로 보는 구조
완성된 시스템 구조를 먼저 보여드릴게요. 최종 형태를 알고 있으면 각 단계를 따라가기가 쉽거든요. 물론 이 아키텍처 구조도는 클로드가 만들어줬습니다.
Slack API (conversations.history, users.info, conversations.replies)
↓ HTTP Callout (Bearer Token)
Apex: SlackService.cls → 메시지 파싱 & Salesforce 저장
↓ Upsert (Message_Timestamp__c 기준 중복 방지)
Custom Object: Slack_Message__c (11개 필드)
↓ SOQL Query
Apex: SlackMessageController.cls (@AuraEnabled)
↓ Wire Service
LWC: slackMessages (html/js/css/meta.xml)
↓ 렌더링
Account Record Page (Lightning App Builder)미들웨어 vs Apex: 질문을 통해서 클로드에게 배우는 아키텍처 구조
Phase 1을 시작하기 전에 Claude가 아키텍처 선택지를 두 개 제안했습니다.
경로 1은 미들웨어(AWS Lambda) 방식이었습니다. Slack Events API가 메시지를 실시간으로 Lambda에 쏘고, Lambda가 Salesforce에 저장하는 흐름이었죠. 경로 2는 Apex 직접 호출 방식이었습니다. Salesforce 자체가 15분마다 Slack API를 호출해서 새 메시지를 가져오는 방식이고요.
Claude는 경로 2(Apex)를 추천했고, 저도 "미들웨어 환경도 없으니 그렇게 하자"고 했는데... 그냥 넘어가기가 좀 찜찜했습니다. 그래서 질문을 하나 했습니다.
"근데 왜 처음에 미들웨어 얘기한 거야? 미들웨어가 더 좋은 이유가 있는 거야?"
미들웨어가 기술적으로 더 "정석"인 이유가 4가지나 되더라고요.
첫째, 실시간성. Slack Events API는 누군가 메시지를 보내는 순간 webhook으로 쏴줍니다. 그러면 1~2초 안에 Salesforce에 반영이 돼요. 반면 Apex Scheduled Job은 최소 실행 간격이 보통 5~15분이라 "거의 실시간"이지 진짜 실시간은 아닌 거죠.
둘째, Salesforce API 한도 문제. Apex에서 Slack API를 주기적으로 호출하면 일일 API 호출 한도를 먹습니다. 미들웨어를 두면 Salesforce에는 꼭 필요할 때만 호출하니까 효율적이고요.
셋째, 확장성. 고객사가 10곳, 20곳으로 늘어나면 채널도 그만큼 늘어나는데, 미들웨어가 있으면 그 확장이 훨씬 쉽습니다.
넷째, 에러 핸들링. Slack API가 일시적으로 응답 안 하거나 rate limit에 걸렸을 때, 미들웨어에서는 retry 로직을 자유롭게 넣을 수 있는데 Apex는 제약이 있다고 하더라고요.
그런데 여기서 또 하나 의문이 들었습니다.
"잠깐, Apex 자체가 대용량 처리에 최적화되어 있지 않아? 수백만 건 레코드도 처리하는 플랫폼이잖아."
이게 사실 절반만 맞는 말이었습니다. Claude가 설명해주길, Apex가 Salesforce 내부 데이터를 처리하는 건 맞아요. 수백만 건을 배치로 돌리는 것도 잘 하고요. 근데 "밖으로 HTTP 요청을 보내는 것"은 다른 문제라고 했습니다.
Salesforce는 멀티테넌트 아키텍처? 처음 들어보는 단어였습니다. Claude가 설명해주기를 한 서버에 수천 개 회사가 같이 올라가 있는 구조예요. 한 회사가 외부 API를 무한정 호출하면 다른 회사 성능에 영향을 주니까, "한 트랜잭션당 callout 100회", "일일 외부 호출 한도" 같은 가드레일을 정책적으로 걸어놓은 거라고 하더라고요. 내부 처리 능력의 한계가 아니라, 공유 자원을 보호하기 위한 설계 결정인 거였습니다.
이걸 이해하고 나서 드는 생각이 있었습니다. 처음에 제가 틀린 가정을 갖고 질문했는데, 오히려 그래서 더 깊은 이해를 하게 된 거잖아요. 그냥 "Apex로 하면 됩니다"만 들었으면 왜 그런지 몰랐을 텐데.
그래서 결론은 이랬습니다. 미들웨어는 "제품화"할 때 필요하고, 지금은 "동작하는 프로토타입"을 만드는 단계였습니다. 지금 Slack Connect 채널이 10곳 이내이고, 하루 메시지도 10~20건 수준이니 API 한도 걱정은 나중 이야기였어요. 동작하는 걸 먼저 만들고, 고객사가 10곳이 되면 그때 아키텍처를 올리면 된다고요.
Claude가 비유를 두 개 들어줬는데, 이게 꽤 와닿았습니다.
"Apex에서 전부 하는 건, 승호가 XX 세일즈코칭도 하고 CRM 세팅도 하고 채용 JD도 쓰고 콜드콜 리뷰도 혼자 다 하는 거야. 한 명이 다 하면 빠르긴 한데, 승호가 아프면 전부 멈추잖아. 미들웨어를 두는 건, Slack 메시지 수집하는 역할을 전담 SDR에게 맡기는 것과 같아. 그 사람이 문제 생겨도 Salesforce는 멀쩡하고, Salesforce가 점검 들어가도 Slack 메시지는 계속 쌓여 있다가 나중에 밀어넣으면 돼."
저를 이해하고 있는 상태에서 해주는 천재적인 비유였습니다. Ep.6에서 "도메인 지식이 AI랑 협업할 때 핵심"이라고 했는데, 반대 방향도 있더라고요. AI가 새로운 개념을 설명할 때, 내가 아는 언어로 번역해서 가르쳐달라고 요청하는 것. 그러면 아키텍처 같은 추상적인 개념도 훨씬 빠르게 체득이 됩니다.
Step 1: Slack App 생성 및 Bot Token 발급
먼저 Slack 쪽 설정부터 했습니다.
Slack API 사이트에서 새 앱을 만들었습니다. 이름은 "Salesforce Slack Sync". Bot Token Scope으로 세 가지를 추가했어요.
channels:history— 퍼블릭 채널 메시지 읽기groups:history— 프라이빗 채널(Slack Connect 포함) 메시지 읽기channels:read— 채널 목록 조회users:read— 메시지 보낸 사람의 이름과 프로필 사진 가져오기
왜 groups:history가 필요하냐면, Slack Connect 채널은 내부적으로 "프라이빗 그룹"으로 분류되기 때문입니다. channels:history만 있으면 Slack Connect 메시지를 못 읽는거죠.
Bot Token(xoxb-로 시작하는 문자열)을 받으면, 이걸 Salesforce에 안전하게 저장해야 합니다. 코드에 토큰을 직접 박으면 보안 문제가 생기거든요. Custom Metadata Type(Slack_Config__mdt)을 만들어서 거기에 저장했습니다. Salesforce가 제공하는 보안 저장소를 활용한 겁니다.
마지막으로 중요한 것: 봇을 해당 Slack Connect 채널에 초대해야 합니다. 채널에서 /invite @Salesforce Slack Sync 명령어를 치면 됩니다. 봇이 채널에 들어가 있지 않으면 API 호출이 not_in_channel 에러를 뱉습니다.
팁: 실제 고객 채널에 바로 연동하지 말고, 테스트 채널을 하나 만들어서 먼저 검증하세요. 저도
#ext-sales-first-people이라는 테스트 채널에서 모든 개발과 디버깅을 끝낸 후에 실제 고객 채널로 전환했습니다.
Step 2: Salesforce 설정 — Remote Site와 Custom Object
Remote Site Settings
Salesforce Apex에서 외부 API를 호출하려면 반드시 해당 도메인을 Remote Site Settings에 등록해야 합니다. Setup → Remote Site Settings에서 https://slack.com을 추가했습니다. 이걸 안 하면 Apex에서 Slack API를 호출할 때 "Unauthorized endpoint" 에러가 나요.
Custom Object: Slack_Message__c
Slack에서 가져온 메시지를 저장할 그릇이 필요합니다. Slack_Message__c라는 Custom Object를 만들고 총 11개 필드를 정의했습니다.
필드명 | 타입 | 용도 |
|---|---|---|
Message_Text__c | Long Text Area (32768) | 메시지 본문. Slack 메시지가 길 수 있으니 넉넉하게 |
Sender_Name__c | Text (255) | 보낸 사람 표시 이름 |
Sender_User_Id__c | Text (50) | Slack User ID (U로 시작) |
Sender_Avatar_Url__c | URL | 프로필 사진 URL |
Message_Timestamp__c | Text (50) | Unique + External ID — 이게 핵심 |
Sent_DateTime__c | DateTime | 메시지 발송 시각 |
Channel_Id__c | Text (50) | Slack 채널 ID |
Channel_Name__c | Text (255) | 채널 표시 이름 |
Is_Bot__c | Checkbox | 봇 메시지 여부 |
Account__c | Lookup(Account) | 연결된 고객사 |
Thread_Timestamp__c | Text (50) | 스레드 부모 메시지 타임스탬프 |
주의해서 봐야 할 필드는 Message_Timestamp__c입니다. 이걸 Unique + External ID로 설정한 이유가 있어요. Slack의 메시지 타임스탬프는 채널 내에서 고유값이거든요. 이걸 External ID로 쓰면 upsert 할 때 자동으로 중복을 방지할 수 있습니다. 15분마다 배치로 메시지를 가져오는 구조에서 같은 메시지가 두 번 저장되는 걸 막아주는 안전장치입니다.
Step 3: Apex 클래스 개발
Production org의 현실
여기서 Production org 특유의 제약을 만났습니다. 저는 Production org(entryai.my.salesforce.com)에서 작업하고 있었는데, Production에서는 Developer Console으로 Apex Class를 직접 생성할 수 없습니다. 반드시 VS Code에서 코드를 작성하고 Salesforce CLI로 배포해야 하고, 배포할 때 Apex 테스트 커버리지가 75% 이상이어야 합니다.
즉, 기능 코드를 만들면 반드시 테스트 코드도 함께 만들어야 한다는 겁니다. 총 6개 Apex 파일이 필요했어요. 그리고 그 과정에서 자연스레 비쥬얼스튜디오 코드(VS Code)도 설치하게 되었습니다.
그리고 지난번처럼 아무것도 모르는 상태에서 VS code 를 처음 설치해서 사용하다보니 이런 질문(어디에다 타이핑해야 하는지도 모르는) 해가면서 하나하나 따라갔습니다. 지금 다시봐도 정말 아무것도 몰랐네요 ^^
그래도 클로드 선생님이 너무 친절하게 잘 가르쳐줘서 계속 단계별로 하나씩 이해해가면서 넘어갔습니다. 세일즈포스에서 동작하게 될 4개의 파일이 저장될 클래스까지 만들었죠.
3개의 기능 클래스
SlackService.cls — 메인 서비스 클래스입니다.
역시나 몰라서 클로드에게 물어보니 족집게 과외 선생님처럼 알려주십니다.
Slack API의 conversations.history를 호출해서 채널 메시지를 가져오고, users.info로 보낸 사람의 표시 이름과 프로필 사진 URL을 조회하고, JSON 응답을 파싱해서 Slack_Message__c로 upsert합니다. Slack API가 메시지에 User ID만 담아서 보내기 때문에, 이름을 알려면 별도 API 호출이 필요해요.
// conversations.history 호출 핵심 부분
HttpRequest req = new HttpRequest();
req.setEndpoint('https://slack.com/api/conversations.history?channel=' + channelId + '&limit=50');
req.setMethod('GET');
req.setHeader('Authorization', 'Bearer ' + botToken);
req.setHeader('Content-Type', 'application/json');
Http http = new Http();
HttpResponse res = http.send(req);SlackMessageScheduler.cls — 15분 간격으로 자동 동기화하는 스케줄러입니다. @future(callout=true) 어노테이션으로 비동기 HTTP 호출을 처리합니다. 포인트는 이 부분:
// Slack_Channel_Id__c가 있는 모든 Account를 자동으로 순회
List<Account> accounts = [
SELECT Id, Slack_Channel_Id__c, Slack_Channel_Name__c
FROM Account
WHERE Slack_Channel_Id__c != null
];
for (Account acc : accounts) {
SlackService.syncMessages(
acc.Slack_Channel_Id__c,
acc.Slack_Channel_Name__c,
acc.Id
);
}이 구조 덕분에 새 고객사가 추가되어도 코드를 수정할 필요가 없습니다. Account 레코드에 채널 ID만 입력하면 자동으로 동기화가 시작돼요. 처음에는 채널 ID를 코드에 하드코딩했다가 나중에 이 방식으로 바꿨는데, 처음부터 이렇게 했어야 했습니다.
SlackMessageController.cls — LWC의 백엔드입니다. @AuraEnabled 메서드 두 개를 제공합니다.
LWC 에 대해서도 너무 대략 이해하고 있는 것 같아서 한참 진행하다가 중간에 물어봤습니다. 역시나 머리에 쏙쏙 박히게 알려줘서 이제는 개념이 잡혔습니다.
@AuraEnabled(cacheable=true)
public static List<Slack_Message__c> getMessages(Id accountId) {
// 해당 Account의 Slack 메시지를 시간순으로 조회
}
@AuraEnabled
public static void syncNow(Id accountId) {
// 수동으로 즉시 동기화 트리거
}3개의 테스트 클래스
각 기능 클래스마다 대응하는 테스트 클래스가 필요합니다. 테스트에서 중요한 건 HttpCalloutMock입니다. 실제 Slack API를 호출하면 테스트 환경에서 에러가 나니까, 가짜 HTTP 응답을 만들어서 테스트를 돌리는 거예요.
// Mock 클래스 예시 — 실제 Slack API 응답을 흉내냄
global class SlackMockHttpResponse implements HttpCalloutMock {
global HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"ok":true,"messages":[{"type":"message","user":"U123","text":"테스트 메시지","ts":"1708300000.000001"}]}');
return res;
}
}프로젝트 폴더 구조
~/slack-integration/
├── force-app/main/default/
│ ├── classes/ (12 파일: .cls 6개 + .cls-meta.xml 6개)
│ └── lwc/slackMessages/ (4 파일: html, js, css, meta.xml)
├── sfdx-project.json
└── package.json총 16개 파일입니다. Claude가 코드를 만들어주고, 저는 VS Code에 붙여넣고 Salesforce CLI로 배포하는 역할이었어요.
Step 4: 첫 배포 — 에러 반복
솔직히 말하면, 한 번에 배포가 성공한 적이 없습니다. 매번 에러가 나고, 원인을 파악하고, 고치고, 다시 배포하는 사이클을 반복했어요.
에러 1: cls-meta.xml에 설명 텍스트가 섞여 들어감
Claude가 작성한 코드를 복사해서 붙여넣는 과정에서 meta.xml 파일에 설명 텍스트가 같이 들어갔습니다. meta.xml 파일은 정확히 이 5줄만 있어야 합니다:
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<status>Active</status>
</ApexClass>한 줄이라도 더 있으면 배포가 실패하더라구요. AI와 작업할 때 코드 블록 경계를 정확히 구분해서 복사해야 해요.
에러 2: 테스트 커버리지 75% 미달
syncNow 메서드를 테스트하지 않아서 커버리지가 모자랐습니다. Production 배포의 75% 규칙은 "기능 하나를 추가하면 테스트도 반드시 함께 보강해야 한다"는 뜻입니다. 처음에는 귀찮았는데, 나중에 보니 이 규칙 덕분에 배포 후 런타임 에러가 없었습니다.
에러 3: Account 이름 불일치
코드에 저도 모르게'서비스명'으로 하드코딩했는데 실제 Account 이름은 해당 회사의‘회사명’이였습니다. 단순 실수인데, 이래서 하드코딩이 문제더라구요. 나중에 Account 필드에서 채널 정보를 동적으로 읽어오는 방식으로 바꾼 이유가 이것입니다.
에러 해결의 패턴
에러가 나면 이런 순서로 해결했습니다:
에러 메시지를 그대로 복사
Claude에게 보여줌
Claude가 원인과 수정 코드를 알려줌
수정 후 재배포
이 사이클이 보통 5~10분 안에 끝났거든요. 예전에 제가 같이 일하던 개발자들이 하던 방식을 떠올려보면(물론 이것도 너무 옛날 이야기일 수도 있습니다만) Stack Overflow를 뒤지고, Salesforce Trailblazer Community를 검색하고, 문서를 읽는 데만 한 시간은 걸렸을 겁니다. 클로드 덕분에 이 디버깅 루프가 확실히 빨라졌습니다.
Step 5: 첫 동기화 성공
배포가 성공하고, Developer Console에서 수동 동기화를 실행했습니다.
SlackMessageScheduler.syncAllChannels();중간에 또 클로드가 말하는 데이터의 정의가 궁금해서 질문했습니다. 조금이라도 뭔가 석연치 않거나 잘 모르겠으면 항상 질문하는 습관이 클로드와 함께 할 때 매우 유용합니다.
Status: Success. 그리고 SOQL 쿼리를 돌렸습니다.
SELECT Sender_Name__c, Message_Text__c, Sent_DateTime__c
FROM Slack_Message__c
ORDER BY Sent_DateTime__c DESC
LIMIT 2020개 행이 나왔습니다. 실제 대화가 보낸 사람 이름, 시간과 함께 Salesforce에 들어와 있었어요 !! 이걸 보니 아 이제 데이터가 들어오는 흐름 자체는 구현되었으니 이걸 화면으로 이쁘게 보여주는 과정이 남았겠구나 라는 생각이 들었습니다. 전체적인 구조에 대한 이해도가 과정속에서 높아지는 것이 저 스스로도 느껴져서 기뻤습니다.
그리고 생각한대로 Account 레코드 페이지에 Slack Messages 컴포넌트를 올리니, Slack 대화가 Salesforce 안에서 바로 보였습니다. 제일 기쁜 순간이었습니다 !
Step 6: UI를 조금 더 끌어올리기
기능은 작동하는데 UI가 문제였습니다. Salesforce 네이티브 Slack Channel 컴포넌트와 나란히 놓으면 차이가 선명하더라고요.
그런데 한 가지 걸렸습니다. Salesforce가 Slack을 인수했으니까 네이티브 UI를 독점적으로 쓰는 건 아닌지? 우리가 원천적으로 같은 수준을 못 만드는 건 아닌지?
Claude한테 물어봤더니 답이 단순했습니다. "기술적 제한 없다. Salesforce 네이티브 컴포넌트도 같은 Slack API(conversations.history, users.info)에서 데이터를 가져오는 것이고, 차이는 프론트엔드 UI 렌더링 품질이다."
이게 이번에 알게 된 핵심이었습니다. API 접근 권한이 아니라 CSS, HTML, JavaScript 작업 품질의 차이일 뿐이라는 것. 그러면 따라잡을 수 있거든요.
Chrome DevTools로 역분석
근데 Claude가 CSS 수치 같은 건 정확히 모른다고 했어요. "Slack 소스코드를 본 적이 없어서 추측이다. 정확하게 맞추려면 네이티브 컴포넌트를 Chrome DevTools로 inspect해서 실제 CSS 값을 줘야 한다."
F12를 열고 네이티브 컴포넌트 요소를 하나씩 클릭해서 실제 값들을 읽었습니다.
요소 | 기존 값 | 네이티브 실측값 |
|---|---|---|
메시지 본문 font-size | 15px | 13px |
보낸 사람 이름 font-weight | 900 | 700 |
아바타 크기 | 36×36px | 32×32px |
타임스탬프 color |
| #5C5C5C |
reply 링크 color |
| #0250D9 |
이 수치들을 Claude에게 넘겼더니, 조금씩 더 나은 UI가 나왔습니다. 추측 기반이 아니라 실측값을 주니까 반복 횟수가 확실히 줄었어요. 하지만 한땀한땀 하기에는 너무 컴포넌트 요소가 많아서 적당히 하다가 멈췄습니다. 아니면 끝도 없겠더라구요. 아래에는 스크린샷 하나만 첨부했지만 실제로는 이 부분에서만 수십번 왔다갔다 했습니다.
앞으로 개선할 것들
DevTools 분석을 통해 다음 단계도 파악이 됐습니다. 프로필 사진을 이니셜 원 대신 실제 users.info API의 profile.image_72로 교체하는 것, Slack mrkdwn 포맷(*굵게*, _기울임_, <URL|링크텍스트>)을 HTML로 변환하는 것, 스레드 답글을 인라인으로 펼치는 것. 구조적으로 어떻게 구현하면 되는지는 파악이 됐고, 다음 이터레이션에서 순서대로 적용할 예정입니다.
Step 7: 자동 동기화 스케줄러 설계
SlackMessageScheduler.cls에는 15분 간격으로 자동 동기화하는 scheduleJob() 메서드를 만들어뒀습니다. 호출하면 매시 0분, 15분, 30분, 45분에 Cron Job이 등록되는 구조예요.
public static void scheduleJob() {
System.schedule('Slack Message Sync - 00', '0 0 * * * ?', new SlackMessageScheduler());
System.schedule('Slack Message Sync - 15', '0 15 * * * ?', new SlackMessageScheduler());
System.schedule('Slack Message Sync - 30', '0 30 * * * ?', new SlackMessageScheduler());
System.schedule('Slack Message Sync - 45', '0 45 * * * ?', new SlackMessageScheduler());
}현재는 수동 Refresh 버튼으로 동기화하고 있고, scheduleJob() 활성화는 아직 하지 않았습니다. 지금은 고객사가 한 곳이고 메시지 빈도도 낮아서 수동으로도 충분하거든요. 고객사가 늘어나면 그때 켜면 됩니다.
Step 8: 테스트 채널에서 실제 고객 채널로 전환
모든 개발을 테스트 채널에서 완료한 후, 실제 고객 채널로 전환했습니다.
전환 과정이 간단했던 이유는 채널 ID가 코드에 하드코딩되어 있지 않기 때문입니다. Account 레코드의 Slack_Channel_Id__c 필드값만 바꾸면 끝이었어요.
실제 고객 채널에 봇을 초대하고, Account 페이지에서 Refresh 버튼을 눌렀을 때 실제 Slack Connect 대화가 Salesforce 안에 나타났습니다.
한 가지 이슈가 있었는데, 테스트 채널과 실제 고객 채널 양쪽의 메시지가 같은 Account에 섞여서 표시되는 문제가 생겼습니다. 기존 테스트 데이터를 삭제하고, SOQL 쿼리에 채널 필터를 추가해서 해결했습니다.
배운 것들
모르는 건 장벽이 아니라 그냥 "다음 질문"이었다
이번에 에러가 총 20번 이상 났습니다.
배포할 때마다 다른 에러가 나왔고, 그때마다 한 일은 동일했어요. 에러 메시지를 그대로 복사해서 Claude에게 보여줬습니다. 원인 파악, 수정 코드 수령, 재배포. 이 사이클의 반복이었습니다.
어느 순간부터 에러를 마주쳐도 "또 에러 났네" 하고 덤덤해지더라고요. 에러가 무서운 게 아니라 그냥 다음 질문이 된 거예요. Ep.6에서 터미널 글자 크기부터 물어봤던 사람이, 이번엔 20번 넘게 막혔는데 그냥 계속 물어봤습니다. 막히면 물어보면 된다는 걸 몸으로 배웠어요.
물어볼수록 더 빨리 이해했다
Claude가 Apex를 추천했을 때 그냥 따라갈 수도 있었습니다. 근데 "왜 처음에 미들웨어 얘기한 거야?"라고 물어봤어요. 그랬더니 Salesforce 멀티테넌트 아키텍처, 외부 HTTP 호출 한도가 왜 존재하는지까지 이해가 됐거든요.
추상적인 개념이 안 잡힐 때는 "내가 아는 언어로 설명해줘"라고 요청했습니다. 그랬더니 이런 비유가 나왔어요.
"미들웨어를 두는 건 Slack 수집 전담 SDR 한 명을 두는 것과 같아. 그 사람이 문제 생겨도 Salesforce는 멀쩡하고, Salesforce가 점검 들어가도 Slack 메시지는 계속 쌓여 있다가 나중에 밀어넣으면 돼."
아키텍처가 그제야 머리에 들어왔습니다. 그냥 "알겠다"하고 넘어가지 말고, 석연치 않으면 바로 물어보는 게 훨씬 빠르다는 걸 이번에 다시 확인했습니다.
Developer Console에서 데이터가 처음 들어왔을 때
배포 성공하고 Developer Console에서 SOQL 쿼리를 실행했습니다. 보낸 사람 이름, 메시지 내용, 발송 시각이 Salesforce 안에서 행으로 쭉 나열되는 걸 처음 봤을 때가 제일 기뻤어요.
근데 기쁜 것보다 더 중요한 게 있었습니다. 데이터가 실제로 흘러들어오는 걸 눈으로 확인하는 순간, "이제 이걸 화면으로 보여주는 것만 남았다"는 게 명확하게 보였거든요. 전체 구조에 대한 이해도가 과정 속에서 높아지는 게 스스로 느껴졌습니다. 코딩을 몰라도, 단계를 하나씩 넘어가다 보면 그냥 보이게 되더라고요.
Slack API는 독점이 아니었다
이번에 알게 된 것 중 제일 의외였습니다. Salesforce 네이티브 Slack Channel 컴포넌트가 쓰는 API랑, 제가 만든 컴포넌트가 쓰는 API가 같아요. conversations.history, users.info — 누구나 쓸 수 있는 겁니다. 네이티브 컴포넌트의 우위는 API 접근 권한이 아니라 CSS 작업 품질의 차이일 뿐이고요. 그러면 따라잡을 수 있다는 거잖아요.
"안 된다"는 말이 진짜 안 된다는 뜻이 아닐 수 있습니다.
도메인 지식이 있으면, 나머지는 질문으로 채울 수 있다
Apex를 한 줄도 써본 적 없었습니다. VS Code가 뭔지도 몰랐고요. 근데 6개의 Apex 클래스와 LWC를 Production에 배포했어요.
물론 아무것도 모르는 상태에서 시작한 건 아닙니다. Salesforce 구조가 어떻게 되어 있는지, 뭘 만들어야 하는지, 에러 상황을 말로 설명할 수 있는 능력. 이건 필요했어요. "어떻게"는 AI가, "무엇을"은 사람이 — 이 구조가 이번에도 맞았습니다.
다음엔 VS Code도 없이 Claude Code만으로 도전해볼 생각입니다. VS Code가 뭔지도 몰랐던 사람이 여기까지 왔으니까요.
최종 상태 & 다음 단계
현재 완성된 것
Slack Connect 메시지가 Salesforce Account 페이지에 표시됨
15분 간격 자동 동기화 + 수동 Refresh 버튼
새 고객사 추가 시 코드 변경 없이 Account 필드만 입력하면 자동 연동
Production 환경 전체 테스트 커버리지 통과 상태로 운영 중
다음 단계
이번 프로젝트를 하면서 Salesforce 내부 구조에 대한 이해가 훨씬 깊어졌습니다. 그래서 다음은 방향이 조금 다릅니다. Salesforce에 직접 들어가지 않고, MCP 서버를 연결해서 Claude를 메인 에이전트로 두고 대화형으로 Salesforce Activity를 업데이트하는 것입니다. 채팅창에서 "오늘 미팅 내용 Salesforce에 기록해줘"라고 말하면 되는 대화형 에이전트 말이죠.