[블로그] Slack Socket Mode로 슬랙에서 상호작용하기

왔다 정보리·2026년 4월 9일
post-thumbnail

이전에 Slack Interactivity를 Request URL 방식으로 구성했는데, Slack Socket Mode를 사용하면 이 과정 없이 WebSocket으로 바로 연결할 수 있다는 걸 알게 되었다. 그러면서 기존에 Slack Interactivity로 작성했던 코드를 Slack Socket Mode로 모두 변경하는 작업을 진행했다.


Slack Socket Mode란?


Socket Mode

Socket Mode는 Slack이 HTTP Request URL 대신 WebSocket 연결을 통해 페이로드를 전달하는 방식이다.

기존 Request URL 방식은 Slack이 우리 서버로 HTTP 요청을 직접 보내는 구조이기 때문에, 서버가 외부에서 접근 가능한 공개 URL을 가지고 있어야 한다. 반면 Socket Mode는 서버가 Slack에 WebSocket 연결을 먼저 열고, Slack은 그 연결을 통해 이벤트를 전달하는 방식이라 공개 URL이 필요 없다.

Socket Mode는 내부 도구나 개발 환경에 적합하다. 지속적인 WebSocket 연결을 유지해야 하고, 연결이 끊기면 재연결 처리가 필요하기 때문에 불특정 다수에게 공개되는 앱에는 Request URL 방식이 더 안정적일 수 있다.

Request URLSocket Mode
연결 방식Slack → 서버 (HTTP)서버 → Slack (WebSocket)
공개 URL 필요 여부필요불필요
ngrok 필요 여부필요불필요
적합한 환경프로덕션, 공개 앱내부 도구, 로컬 개발

Slack Socket Mode 구현하기


1. Socket Mode 활성화

Socket Mode

Slack API에서 대상 앱으로 이동한 뒤, Settings → Socket Mode 메뉴에서 Enable Socket Mode 토글을 활성화한다.
활성화하면 기존에 설정했던 Request URL 방식은 비활성화되고, 이후 모든 페이로드는 WebSocket 연결로 수신된다.

2. App-Level Token 발급

Scope

Socket Mode를 사용하려면 일반 Bot 토큰과는 별도로 App-Level Token이 필요하다.

Settings → Basic Information → App-Level Tokens에서 토큰을 생성할 수 있다. 이때 connections:write scope를 반드시 추가해야 하며, 생성된 xapp-으로 시작하는 토큰을 복사해둔다.

3. 환경 변수 설정

slack:
  oauth-token: ${SLACK_BOT_TOKEN}           # Bot User OAuth Token (xoxb-)
  app-level-token: ${SLACK_APP_LEVEL_TOKEN} # App-Level Token (xapp-)
  socket-mode-enabled: true

각 항목의 역할은 다음과 같다.

  • oauth-token : Slack API 호출에 사용하는 Bot 토큰
  • app-level-token : Socket Mode WebSocket 연결에 사용하는 토큰으로, 위에서 복사해둔 xapp- 값을 사용한다
  • socket-mode-enabled : false로 설정하면 Socket Mode 관련 빈이 등록되지 않아 손쉽게 on/off 전환이 가능하다

토큰 값은 외부에 노출되면 안 되므로 반드시 환경 변수로 관리해야 한다.

4. SlackProperties 설정

@ConfigurationProperties(prefix = "slack")
@Getter
@RequiredArgsConstructor
public class SlackProperties {
    private final String oauthToken;
    private final String appLevelToken;
    private final boolean socketModeEnabled;
}

@ConfigurationProperties를 사용해 yaml의 slack.* 값을 하나의 클래스로 바인딩한다.
이후 빈으로 주입받아 사용하면 된다.

5. SlackConfig 설정

@Configuration
@EnableConfigurationProperties(SlackProperties.class)
@RequiredArgsConstructor
public class SlackConfig {
    private final SlackProperties slackProperties;

    @Bean
    public MethodsClient methodsClient() {
        Slack slack = Slack.getInstance();
        return slack.methods(slackProperties.getOauthToken());
    }

    @Bean
    @ConditionalOnProperty(name = "slack.socket-mode-enabled", havingValue = "true")
    public String slackSocketModeInfo() {
        log.info("Slack Socket Mode is ENABLED - using WebSocket connection");
        return "socket-mode";
    }
}

MethodsClient를 빈으로 등록해 Slack API를 호출할 수 있도록 하고, @ConditionalOnProperty를 통해 socket-mode-enabled: true일 때만 Socket Mode 관련 빈이 활성화되도록 구성한다.

6. SlackSocketModeClient 설정

@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "slack.socket-mode-enabled", havingValue = "true")
public class SlackSocketModeClient {

    private final SlackProperties slackProperties;
    private final SlackSocketModeService slackSocketModeService;
    private SocketModeApp socketModeApp;

    @PostConstruct
    public void initialize() {
        App app = new App(AppConfig.builder()
                .singleTeamBotToken(slackProperties.getOauthToken())
                .build());

        // 정규식 패턴으로 모든 블록 액션을 캐치
        app.blockAction(Pattern.compile(".*"), (req, ctx) -> {
            slackSocketModeService.handleBlockAction(req, ctx);
            return ctx.ack();
        });

        socketModeApp = new SocketModeApp(slackProperties.getAppLevelToken(), app);

        Thread socketModeThread = new Thread(() -> socketModeApp.start());
        socketModeThread.setName("slack-socket-mode");
        socketModeThread.setDaemon(true);
        socketModeThread.start();
    }

    @PreDestroy
    public void shutdown() {
        if (socketModeApp != null) {
            socketModeApp.stop();
        }
    }
}

@PostConstruct로 애플리케이션 시작 시 WebSocket 연결을 자동으로 초기화하고, @PreDestroy로 종료 시 연결을 안전하게 닫는다. blockAction(Pattern.compile(".*"))으로 모든 액션을 단일 핸들러에서 수신한 뒤 actionId 기준으로 분기 처리하는 구조다. 한 가지 주의할 점은 Socket Mode가 블로킹 방식으로 동작하기 때문에, 별도 데몬 스레드에서 실행하지 않으면 메인 스레드가 블로킹된다는 것이다.

7. SlackSocketModeService 설정

@Slf4j
@Service
@RequiredArgsConstructor
public class SlackSocketModeService {

    public void handleBlockAction(BlockActionRequest request, ActionContext context) {
        String actionId = request.getPayload().getActions().get(0).getActionId();
        String userId = request.getPayload().getUser().getId();
        String value = request.getPayload().getActions().get(0).getValue();

        // TODO: actionId에 따른 비즈니스 로직 처리 (비동기 권장)
        switch (actionId) {
            default:
                log.info("Unknown action ID: {}", actionId);
        }
    }
}

수신된 블록 액션을 actionId 기준으로 분기해 비즈니스 로직을 처리한다. 주의할 점은 Slack이 일정 시간 내에 반드시 ack()를 요구한다는 것이다. 따라서 무거운 처리 로직은 비동기로 분리하는 것을 권장한다.

마지막으로

외부에 공개할 필요 없는 내부용 앱이라면 Socket Mode가 더 나은 선택일 수 있으니, 상황에 맞게 Request URL 방식과 비교해서 골라보자!


profile
왔다 정보리

0개의 댓글