문제: 그룹웨어 API를 어떻게 호출하나

사내 그룹웨어는 브라우저에서 로그인한 세션 쿠키로 API를 호출하는 구조다. 공개 API키 같은 게 없고, 그냥 브라우저처럼 로그인해서 쿠키를 들고 API를 쳐야 한다.

Node.js에서 이걸 하려면 axios로 로그인 과정을 그대로 흉내내야 한다.


SSO 로그인 흐름

대부분의 사내 SSO는 아래 패턴을 따른다.

1단계: 로그인 페이지 GET → 초기 세션 쿠키 획득
2단계: 로그인 form POST → 인증 처리
3단계: SSO 토큰 발급 페이지 GET → 리다이렉트로 토큰 획득
4단계: 대상 도메인에 토큰으로 접근 → 해당 도메인 세션 쿠키 획득

핵심은 각 단계에서 받은 쿠키를 다음 요청에 그대로 넘겨줘야 한다는 것이다.


axios로 SSO 구현하기

쿠키 파싱 헬퍼

axios는 브라우저와 달리 쿠키를 자동으로 관리해주지 않는다. 응답 헤더에서 직접 파싱해야 한다.

function parseCookies(setCookieHeader) {
  if (!setCookieHeader) return '';
  const cookies = Array.isArray(setCookieHeader)
    ? setCookieHeader
    : [setCookieHeader];
  return cookies
    .map(c => c.split(';')[0])  // 'name=value; Path=/' → 'name=value'
    .join('; ');
}

1단계: 초기 세션 쿠키 획득

const initRes = await axios.get('https://portal.company.com/login', {
  maxRedirects: 0,
  validateStatus: s => s < 400,
});
let cookie = parseCookies(initRes.headers['set-cookie']);

2단계: 로그인

const loginRes = await axios.post(
  'https://portal.company.com/login/check',
  new URLSearchParams({
    id: process.env.LOGIN_ID,
    password: process.env.LOGIN_PW,
  }),
  {
    headers: {
      Cookie: cookie,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    maxRedirects: 0,
    validateStatus: s => s < 400,
  }
);
cookie = parseCookies(loginRes.headers['set-cookie']) || cookie;

로그인 요청은 form-data(application/x-www-form-urlencoded) 방식으로 보내야 한다. axios.post에 객체를 그냥 넘기면 JSON으로 보내져서 로그인이 안 된다. URLSearchParams로 감싸줘야 한다.

3단계: SSO 토큰 발급

const ssoRes = await axios.get('https://portal.company.com/sso-redirect', {
  headers: { Cookie: cookie },
  maxRedirects: 0,  // 리다이렉트 수동으로 따라가야 함
  validateStatus: s => s < 400,
});

// 리다이렉트 URL에서 토큰 추출
const ssoUrl = ssoRes.headers['location'];

maxRedirects: 0이 중요하다. axios가 자동으로 리다이렉트를 따라가면 쿠키가 유실된다. 리다이렉트를 수동으로 처리해야 각 단계의 쿠키를 직접 챙길 수 있다.

4단계: 대상 도메인 세션 획득

const domainRes = await axios.get(ssoUrl, {
  maxRedirects: 5,  // 여기서는 따라가도 됨
  validateStatus: s => s < 400,
});
const domainCookie = parseCookies(domainRes.headers['set-cookie']);

domainCookie가 실제 API 호출에 쓰는 세션이다.


도메인별 세션 분리

우리 그룹웨어는 기능별로 서브도메인이 달랐다. 예를 들면 휴가는 leave.company.com, 회의실은 gw.company.com 식이다.

문제는 도메인이 다르면 쿠키가 공유되지 않는다는 거다. 각 도메인에 별도로 SSO 로그인을 해야 한다.

// 서버 시작 시 두 도메인 모두 로그인
let avsCookie = await login('leave');  // 휴가 도메인
let gwCookie  = await login('gw');     // 회의실 도메인

// API 호출 시 해당 도메인 쿠키 사용
async function getVacation(date) {
  return axios.post(VACATION_API_URL, payload, {
    headers: { Cookie: avsCookie, ...customHeaders },
  });
}

async function getMeetingRooms(date) {
  return axios.post(ROOM_API_URL, payload, {
    headers: { Cookie: gwCookie, ...customHeaders },
  });
}

커스텀 헤더 문제

API를 호출했는데 404나 의미 없는 오류 코드가 계속 반환됐다. 로그인도 됐고 쿠키도 맞는데 왜 안 되나 한참 삽질했다.

브라우저 Network 탭을 열어서 실제 요청을 비교해보니 커스텀 헤더들이 빠져있었다.

const customHeaders = {
  '__service_id__': 'SERVICE_NAME',
  '__view_id__': 'view-identifier',
  '__menu_id__': 'MENU_CODE',
  'ajax': 'true',
  'x-requested-with': 'XMLHttpRequest',
};

그룹웨어 API는 이런 커스텀 헤더로 어떤 서비스/메뉴에서 요청이 왔는지 검증한다. 빠지면 요청이 거부된다.

해결법: 브라우저 Network 탭에서 실제 API 요청을 찾아 Request Headers를 전부 복사해서 동일하게 맞춰줬다.


세션 유지 (30분마다 keepalive)

SSO 세션은 일정 시간 요청이 없으면 만료된다. 봇이 새벽에 아무것도 안 하다가 아침에 알림을 보내려 하면 세션이 끊겨있는 상황이 생긴다.

cron.schedule('*/30 * * * *', async () => {
  try {
    // 세션 페이지에 GET 요청으로 keepalive
    await axios.get(SESSION_KEEP_URL, {
      headers: { Cookie: avsCookie },
    });
    await axios.get(SESSION_KEEP_URL_GW, {
      headers: { Cookie: gwCookie },
    });
  } catch (err) {
    // 실패 시 재로그인
    avsCookie = await login('leave');
    gwCookie  = await login('gw');
  }
});

keepalive 실패 시 재로그인하도록 해두면 세션이 끊겨도 자동 복구된다.


메모리 캐시로 API 호출 줄이기

같은 날짜 데이터를 10분마다 계속 API에서 가져오면 서버에 부담이 된다. 메모리 캐시로 불필요한 호출을 줄였다.

const cache = new Map();
const CACHE_TTL = 15 * 60 * 1000; // 15분

async function fetchWithCache(key, fetchFn) {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.time < CACHE_TTL) {
    return cached.data;
  }
  const data = await fetchFn();
  cache.set(key, { data, time: Date.now() });
  return data;
}

트러블슈팅 정리

증상 원인 해결
로그인 후 API 호출 시 인증 오류 axios가 리다이렉트 자동 처리 중 쿠키 유실 maxRedirects: 0으로 수동 처리
API 404 / 의미 없는 오류 코드 커스텀 헤더 누락 브라우저 Network 탭에서 헤더 전부 확인 후 동일하게 설정
아침 알림 시 세션 만료 SSO 세션 TTL 초과 30분마다 keepalive + 실패 시 재로그인
같은 도메인인데 API마다 세션 다름 서브도메인별 쿠키 분리 도메인별 별도 로그인 세션 관리
form 로그인 안 됨 axios POST에 JSON으로 전송됨 URLSearchParams로 감싸서 form-data 형식으로 전송

다음 편에서는 수집한 데이터를 스냅샷으로 관리하고, 이전 상태와 비교해서 변동을 감지하는 로직과 Slack 알림 발송 구현을 다룬다.