Polling

폴링(Polling)은 API 통합 패턴 중 가장 기본적이면서도 널리 사용되는 방식이다. 이 패턴은 단순하지만 다양한 상황에서 효과적으로 활용될 수 있으며, 올바르게 구현하면 강력한 통합 메커니즘이 될 수 있다.

폴링의 기본 개념

폴링은 클라이언트가 주기적으로 서버에 요청을 보내 새로운 정보나 상태 변화를 확인하는 통신 방식이다. 이는 ‘끌어오기(Pull)’ 방식의 대표적인 예로, 클라이언트가 능동적으로 서버에서 정보를 요청한다.

폴링의 작동 원리

폴링의 기본 작동 과정은 다음과 같다:

  1. 클라이언트가 서버에 정보 요청을 보낸다.
  2. 서버는 현재 상태나 데이터를 응답으로 반환한다.
  3. 클라이언트는 일정 시간(폴링 간격) 동안 대기한다.
  4. 대기 시간이 끝나면 클라이언트는 다시 1단계로 돌아가 요청을 반복한다.

이 과정은 클라이언트가 중단하거나 다른 통신 방식으로 전환할 때까지 계속된다.

폴링이 필요한 이유

폴링은 다음과 같은 상황에서 특히 유용하다:

폴링의 종류

폴링에는 여러 변형이 있으며, 각각 특정 사용 사례에 적합하다.

  1. 단순 폴링(Simple Polling)
    가장 기본적인 형태로, 클라이언트가 고정된 시간 간격으로 서버에 요청을 보낸다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    // 단순 폴링 예시 (JavaScript)
    function simplePolling() {
      const POLLING_INTERVAL = 5000; // 5초마다 폴링
    
      function poll() {
        fetch('/api/status')
          .then(response => response.json())
          .then(data => {
            // 데이터 처리 로직
            console.log('폴링된 데이터:', data);
          })
          .catch(error => {
            console.error('폴링 오류:', error);
          })
          .finally(() => {
            // 다음 폴링 일정 설정
            setTimeout(poll, POLLING_INTERVAL);
          });
      }
    
      // 첫 번째 폴링 시작
      poll();
    }
    
    simplePolling();
    
  2. 롱 폴링(Long Polling)
    롱 폴링은 단순 폴링을 개선한 방식으로, 서버가 새로운 정보를 가지고 있을 때까지 응답을 지연시킨다.

    1. 클라이언트가 서버에 요청을 보낸다.
    2. 서버는 새 정보가 있을 때까지 응답을 보류하고 연결을 유지한다(타임아웃 한도 내에서).
    3. 새 정보가 생기면 서버가 즉시 응답을 보낸다.
    4. 클라이언트는 응답을 받은 후 바로 새 요청을 보낸다.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    // 롱 폴링 예시 (JavaScript)
    function longPolling() {
      function poll() {
        fetch('/api/events/longpoll', {
          timeout: 30000 // 30초 타임아웃
        })
          .then(response => response.json())
          .then(data => {
            // 데이터 처리 로직
            console.log('롱 폴링 데이터:', data);
    
            // 즉시 다음 요청 시작
            poll();
          })
          .catch(error => {
            console.error('폴링 오류:', error);
    
            // 오류 발생 시 짧은 지연 후 재시도
            setTimeout(poll, 5000);
          });
      }
    
      // 첫 번째 롱 폴링 시작
      poll();
    }
    
    longPolling();
    
  3. 적응형 폴링(Adaptive Polling)
    적응형 폴링은 시스템 상태나 이전 응답에 따라 폴링 간격을 동적으로 조정한다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
    // 적응형 폴링 예시 (JavaScript)
    function adaptivePolling() {
      let pollingInterval = 5000; // 기본 간격: 5초
      const MIN_INTERVAL = 1000;   // 최소 간격: 1초
      const MAX_INTERVAL = 60000;  // 최대 간격: 1분
    
      function poll() {
        const startTime = Date.now();
    
        fetch('/api/status')
          .then(response => response.json())
          .then(data => {
            // 데이터 처리 로직
            console.log('폴링된 데이터:', data);
    
            // 변경 사항이 있을 경우 폴링 간격 감소
            if (data.hasChanges) {
              pollingInterval = Math.max(MIN_INTERVAL, pollingInterval / 2);
            } else {
              // 변경 사항이 없을 경우 폴링 간격 증가
              pollingInterval = Math.min(MAX_INTERVAL, pollingInterval * 1.5);
            }
    
            console.log(`다음 폴링 간격: ${pollingInterval}ms`);
          })
          .catch(error => {
            console.error('폴링 오류:', error);
            // 오류 발생 시 폴링 간격 증가
            pollingInterval = Math.min(MAX_INTERVAL, pollingInterval * 2);
          })
          .finally(() => {
            const elapsed = Date.now() - startTime;
            const nextPollIn = Math.max(0, pollingInterval - elapsed);
    
            // 다음 폴링 일정 설정
            setTimeout(poll, nextPollIn);
          });
      }
    
      // 첫 번째 폴링 시작
      poll();
    }
    
    adaptivePolling();
    
  4. 지능형 폴링(Smart Polling)
    지능형 폴링은 서버 상태, 네트워크 조건, 사용자 활동 등 여러 요소를 고려하여 폴링 전략을 최적화한다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    
    // 지능형 폴링 예시 (JavaScript)
    function smartPolling() {
      const config = {
        baseInterval: 5000,
        maxInterval: 120000,
        minInterval: 1000,
        userActiveInterval: 2000,
        userInactiveInterval: 30000,
        errorBackoffMultiplier: 1.5,
        consecutiveErrorsLimit: 5
      };
    
      let currentInterval = config.baseInterval;
      let consecutiveErrors = 0;
      let lastDataTimestamp = null;
      let isUserActive = true;
      let timeoutId = null;
    
      // 사용자 활동 감지
      document.addEventListener('mousemove', handleUserActivity);
      document.addEventListener('keydown', handleUserActivity);
      document.addEventListener('visibilitychange', handleVisibilityChange);
    
      function handleUserActivity() {
        if (!isUserActive) {
          isUserActive = true;
          // 사용자가 활성 상태로 변경되면 즉시 폴링
          if (timeoutId) {
            clearTimeout(timeoutId);
            poll();
          }
        }
    
        // 일정 시간 후 비활성으로 설정
        resetInactivityTimer();
      }
    
      function resetInactivityTimer() {
        if (window.userInactivityTimer) {
          clearTimeout(window.userInactivityTimer);
        }
    
        window.userInactivityTimer = setTimeout(() => {
          isUserActive = false;
          adjustPollingInterval();
        }, 60000); // 1분 동안 활동 없으면 비활성으로 간주
      }
    
      function handleVisibilityChange() {
        if (document.hidden) {
          isUserActive = false;
        } else {
          isUserActive = true;
          // 탭이 다시 활성화되면 즉시 폴링
          if (timeoutId) {
            clearTimeout(timeoutId);
            poll();
          }
        }
    
        adjustPollingInterval();
      }
    
      function adjustPollingInterval() {
        // 사용자 활동 기반 조정
        if (isUserActive) {
          currentInterval = config.userActiveInterval;
        } else {
          currentInterval = config.userInactiveInterval;
        }
    
        // 데이터 변화율 기반 조정
        if (lastDataTimestamp) {
          const dataAge = Date.now() - lastDataTimestamp;
    
          // 데이터가 오래되었으면 더 자주 폴링
          if (dataAge > 60000) { // 1분 이상 경과
            currentInterval = Math.max(config.minInterval, currentInterval / 2);
          }
        }
    
        // 오류 발생 횟수 기반 조정
        if (consecutiveErrors > 0) {
          const errorMultiplier = Math.pow(config.errorBackoffMultiplier, Math.min(consecutiveErrors, config.consecutiveErrorsLimit));
          currentInterval = Math.min(config.maxInterval, currentInterval * errorMultiplier);
        }
    
        console.log(`조정된 폴링 간격: ${currentInterval}ms`);
      }
    
      function poll() {
        fetch('/api/status')
          .then(response => response.json())
          .then(data => {
            // 데이터 처리 로직
            console.log('폴링된 데이터:', data);
    
            // 성공적인 응답 처리
            consecutiveErrors = 0;
            lastDataTimestamp = Date.now();
          })
          .catch(error => {
            console.error('폴링 오류:', error);
            consecutiveErrors++;
          })
          .finally(() => {
            // 폴링 간격 조정
            adjustPollingInterval();
    
            // 다음 폴링 예약
            timeoutId = setTimeout(poll, currentInterval);
          });
      }
    
      // 첫 번째 폴링 시작
      poll();
    
      // 클린업 함수
      return function cleanup() {
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
        document.removeEventListener('mousemove', handleUserActivity);
        document.removeEventListener('keydown', handleUserActivity);
        document.removeEventListener('visibilitychange', handleVisibilityChange);
      };
    }
    
    const cleanup = smartPolling();
    

폴링을 위한 API 설계

효과적인 폴링을 지원하기 위한 API 설계 시 고려해야 할 사항들을 살펴보면:

효율적인 엔드포인트 설계

폴링에 최적화된 API 엔드포인트 설계 원칙:

  1. 리소스 최소화: 불필요한 데이터 전송을 피하고 필요한 정보만 제공
  2. 상태 기반 응답: 클라이언트 상태와 서버 상태를 비교하여 변경된 내용만 전송
  3. 적절한 HTTP 상태 코드 사용: 다양한 시나리오에 맞는 명확한 상태 코드 제공
  4. 캐싱 지원: 효과적인 캐싱을 통한 서버 부하 감소
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 효율적인 폴링 엔드포인트 예시 (Spring Boot)
@RestController
@RequestMapping("/api/polls")
public class PollingController {

    private final PollingService pollingService;
    
    @GetMapping("/status")
    public ResponseEntity<StatusResponse> getStatus(
            @RequestParam(required = false) String lastKnownVersion) {
        
        // 현재 상태 버전 조회
        String currentVersion = pollingService.getCurrentVersion();
        
        // 변경 사항이 없으면 304 Not Modified 반환
        if (currentVersion.equals(lastKnownVersion)) {
            return ResponseEntity
                .status(HttpStatus.NOT_MODIFIED)
                .build();
        }
        
        // 변경 사항이 있으면 새 데이터 제공
        StatusResponse status = pollingService.getStatus();
        
        return ResponseEntity.ok()
            .eTag(currentVersion)  // ETag 헤더 설정
            .cacheControl(CacheControl.noCache())  // 캐싱 제어
            .body(status);
    }
    
    @GetMapping("/events")
    public ResponseEntity<List<Event>> getEvents(
            @RequestParam(required = false) Long afterTimestamp,
            @RequestParam(defaultValue = "100") int limit) {
        
        Long lastEventTimestamp;
        List<Event> events;
        
        if (afterTimestamp != null) {
            // 지정된 타임스탬프 이후의 이벤트만 조회
            events = pollingService.getEventsAfter(afterTimestamp, limit);
            
            // 결과가 없으면 204 No Content 반환
            if (events.isEmpty()) {
                return ResponseEntity
                    .noContent()
                    .build();
            }
        } else {
            // 최신 이벤트 조회
            events = pollingService.getLatestEvents(limit);
        }
        
        // 마지막 이벤트 타임스탬프 계산
        lastEventTimestamp = events.isEmpty() ? 
            System.currentTimeMillis() : 
            events.get(events.size() - 1).getTimestamp();
        
        return ResponseEntity.ok()
            .header("X-Last-Event-Timestamp", lastEventTimestamp.toString())
            .body(events);
    }
}

변경 사항 감지 메커니즘

효율적인 폴링을 위해 서버는 변경 사항을 빠르게 감지하고 전달할 수 있어야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 변경 사항 감지 메커니즘 예시 (Spring Boot)
@Service
public class PollingServiceImpl implements PollingService {

    private final DataRepository dataRepository;
    private final VersionManager versionManager;
    
    // 버전 기반 변경 감지
    @Override
    public String getCurrentVersion() {
        return versionManager.getCurrentVersion();
    }
    
    @Override
    public StatusResponse getStatus() {
        StatusData data = dataRepository.getCurrentData();
        
        return new StatusResponse(
            data,
            versionManager.getCurrentVersion(),
            System.currentTimeMillis()
        );
    }
    
    // 타임스탬프 기반 변경 감지
    @Override
    public List<Event> getEventsAfter(Long timestamp, int limit) {
        return dataRepository.findEventsByTimestampAfter(timestamp, limit);
    }
    
    @Override
    public List<Event> getLatestEvents(int limit) {
        return dataRepository.findLatestEvents(limit);
    }
    
    // 데이터 변경 시 버전 업데이트
    @EventListener
    public void handleDataChangeEvent(DataChangeEvent event) {
        versionManager.incrementVersion();
    }
}

// 버전 관리 서비스
@Service
public class VersionManager {
    
    private AtomicLong version = new AtomicLong(0);
    
    public String getCurrentVersion() {
        return "v" + version.get();
    }
    
    public void incrementVersion() {
        version.incrementAndGet();
    }
}

롱 폴링 구현

서버 측에서 롱 폴링을 구현하는 방법을 살펴보면:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 롱 폴링 구현 예시 (Spring Boot + DeferredResult)
@RestController
@RequestMapping("/api/polls")
public class LongPollingController {

    private final EventService eventService;
    private final ConcurrentMap<String, DeferredResult<ResponseEntity<List<Event>>>> activeRequests = new ConcurrentHashMap<>();
    
    @GetMapping("/events/longpoll")
    public DeferredResult<ResponseEntity<List<Event>>> longPollEvents(
            @RequestParam(required = false) Long afterTimestamp,
            @RequestParam(defaultValue = "30000") long timeout) {
        
        // DeferredResult 생성 (타임아웃 설정)
        DeferredResult<ResponseEntity<List<Event>>> result = new DeferredResult<>(timeout);
        
        // 타임아웃 처리
        result.onTimeout(() -> {
            List<Event> events = eventService.getLatestEvents(10);
            Long lastTimestamp = events.isEmpty() ? 
                System.currentTimeMillis() : 
                events.get(events.size() - 1).getTimestamp();
                
            result.setResult(ResponseEntity.ok()
                .header("X-Last-Event-Timestamp", lastTimestamp.toString())
                .body(events));
        });
        
        // 요청 완료 시 정리
        result.onCompletion(() -> {
            activeRequests.remove(result.toString());
        });
        
        // 현재 이벤트 확인
        if (afterTimestamp != null) {
            List<Event> newEvents = eventService.getEventsAfter(afterTimestamp, 10);
            
            if (!newEvents.isEmpty()) {
                // 이미 새 이벤트가 있으면 즉시 응답
                Long lastTimestamp = newEvents.get(newEvents.size() - 1).getTimestamp();
                
                result.setResult(ResponseEntity.ok()
                    .header("X-Last-Event-Timestamp", lastTimestamp.toString())
                    .body(newEvents));
                    
                return result;
            }
        }
        
        // 요청 등록 (새 이벤트 발생 시 알림 받기 위함)
        String requestId = UUID.randomUUID().toString();
        activeRequests.put(requestId, result);
        
        return result;
    }
    
    // 새 이벤트 발생 시 대기 중인 요청에 알림
    @EventListener
    public void handleEventCreated(EventCreatedEvent createdEvent) {
        Event event = createdEvent.getEvent();
        
        List<Event> events = Collections.singletonList(event);
        
        // 모든 대기 중인 롱 폴링 요청에 응답
        activeRequests.forEach((requestId, deferredResult) -> {
            if (!deferredResult.isSetOrExpired()) {
                deferredResult.setResult(ResponseEntity.ok()
                    .header("X-Last-Event-Timestamp", event.getTimestamp().toString())
                    .body(events));
            }
        });
        
        // 응답한 요청은 맵에서 제거 (이미 onCompletion에서 처리됨)
    }
}

폴링의 장단점

장점

  1. 단순성: 구현이 간단하고 이해하기 쉽다.
  2. 광범위한 호환성: 모든 HTTP 클라이언트와 호환되며 별도의 프로토콜이 필요하지 않다.
  3. 방화벽 친화적: 표준 HTTP 요청을 사용하므로 방화벽이나 프록시 문제가 적다.
  4. 클라이언트 제어: 클라이언트가 요청 빈도와 타이밍을 제어할 수 있다.
  5. 오류 복원력: 일시적인 서버 장애에 강하며, 서버가 다시 사용 가능해지면 자동으로 다시 연결된다.

단점

  1. 리소스 비효율성: 변경 사항이 없어도 지속적으로 요청을 보내므로 네트워크와 서버 리소스를 낭비할 수 있다.
  2. 지연 시간: 폴링 간격에 따라 실시간성이 떨어질 수 있다.
  3. 확장성 문제: 많은 클라이언트가 동시에 폴링하면 서버에 부하가 생길 수 있다.
  4. 불필요한 처리: 변경 사항이 없을 때도 요청과 응답을 처리해야 한다.
  5. 배터리 소모: 모바일 장치에서 지속적인 폴링은 배터리 소모를 증가시킬 수 있다.

폴링 최적화 전략

서버 측 최적화

조건부 요청

HTTP의 ETag 또는 Last-Modified 헤더를 사용하여 변경된 경우에만 데이터를 전송한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 조건부 요청 처리 예시 (Spring Boot)
@GetMapping("/data")
public ResponseEntity<DataResponse> getData(
        @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
    
    // 현재 데이터 버전 확인
    String currentETag = dataService.getCurrentETag();
    
    // ETag가 일치하면 304 Not Modified 반환
    if (currentETag.equals(ifNoneMatch)) {
        return ResponseEntity
            .status(HttpStatus.NOT_MODIFIED)
            .eTag(currentETag)
            .build();
    }
    
    // 변경된 데이터 조회
    DataResponse data = dataService.getCurrentData();
    
    // 200 OK와 함께 데이터 및 ETag 반환
    return ResponseEntity.ok()
        .eTag(currentETag)
        .body(data);
}
부분 응답

변경된 데이터 부분만 응답에 포함시킨다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 부분 응답 구현 예시 (Spring Boot)
@GetMapping("/data/delta")
public ResponseEntity<DataDelta> getDataDelta(
        @RequestParam String baseVersion) {
    
    // 기준 버전에서의 변경 사항 계산
    DataDelta delta = dataService.computeDeltaSince(baseVersion);
    
    // 변경 사항이 없으면 204 No Content 반환
    if (delta.isEmpty()) {
        return ResponseEntity
            .noContent()
            .header("X-Current-Version", dataService.getCurrentVersion())
            .build();
    }
    
    // 변경 내용 반환
    return ResponseEntity.ok()
        .header("X-Current-Version", dataService.getCurrentVersion())
        .body(delta);
}
페이지네이션 및 필터링

큰 데이터셋의 경우 페이지네이션과 필터링을 통해 필요한 부분만 전송한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 페이지네이션 및 필터링 예시 (Spring Boot)
@GetMapping("/events")
public ResponseEntity<Page<Event>> getEvents(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "50") int size,
        @RequestParam(required = false) Long afterTimestamp,
        @RequestParam(required = false) String type) {
    
    // 페이지네이션 및 필터링 정보
    Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
    
    // 필터 적용된 데이터 조회
    Page<Event> events;
    if (afterTimestamp != null && type != null) {
        events = eventRepository.findByTimestampAfterAndType(afterTimestamp, type, pageable);
    } else if (afterTimestamp != null) {
        events = eventRepository.findByTimestampAfter(afterTimestamp, pageable);
    } else if (type != null) {
        events = eventRepository.findByType(type, pageable);
    } else {
        events = eventRepository.findAll(pageable);
    }
    
    return ResponseEntity.ok(events);
}

클라이언트 측 최적화

지수 백오프

오류 발생 시 대기 시간을 점진적으로 늘려 재시도 빈도를 줄인다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 지수 백오프 구현 예시 (JavaScript)
function pollWithExponentialBackoff() {
  let retryCount = 0;
  const MAX_RETRIES = 10;
  const BASE_DELAY = 1000;
  const MAX_DELAY = 60000;
  
  function poll() {
    fetch('/api/status')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error ${response.status}`);
        }
        return response.json();
      })
      .then(data => {
        // 성공적인 응답, 재시도 카운트 초기화
        retryCount = 0;
        console.log('폴링 데이터:', data);
        
        // 정상 간격으로 다음 폴링 예약
        setTimeout(poll, BASE_DELAY);
      })
      .catch(error => {
        console.error('폴링 오류:', error);
        
        // 최대 재시도 횟수 확인
        if (retryCount >= MAX_RETRIES) {
          console.error('최대 재시도 횟수 초과, 폴링 중단');
          return;
        }
        
        // 지수 백오프 계산
        retryCount++;
        const delay = Math.min(MAX_DELAY, BASE_DELAY * Math.pow(2, retryCount));
        console.log(`${delay}ms 후 재시도 (${retryCount}/${MAX_RETRIES})`);
        
        // 계산된 지연 후 다시 시도
        setTimeout(poll, delay);
      });
  }
  
  // 첫 번째 폴링 시작
  poll();
}

pollWithExponentialBackoff();
사용자 활동 기반 폴링

사용자 활동이 있을 때는 더 자주, 비활성 상태일 때는 덜 자주 폴링한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 사용자 활동 기반 폴링 예시 (JavaScript)
function userActivityBasedPolling() {
  const ACTIVE_POLLING_INTERVAL = 5000;    // 활성 상태일 때 5초
  const INACTIVE_POLLING_INTERVAL = 30000; // 비활성 상태일 때 30초
  
  let isUserActive = true;
  let pollingInterval = ACTIVE_POLLING_INTERVAL;
  let inactivityTimeout;
  let pollingTimeout;
  
  // 사용자 활동 감지
  function resetInactivityTimer() {
    // 기존 타이머 취소
    if (inactivityTimeout) {
      clearTimeout(inactivityTimeout);
    }
    
    // 사용자가 비활성 상태였다면 활성 상태로 변경
    if (!isUserActive) {
      isUserActive = true;
      pollingInterval = ACTIVE_POLLING_INTERVAL;
      
      // 빠른 폴링으로 즉시 전환
      if (pollingTimeout) {
        clearTimeout(pollingTimeout);
        poll();
      }
    }
    
    // 비활성 타이머 설정
    inactivityTimeout = setTimeout(() => {
      isUserActive = false;
      pollingInterval = INACTIVE_POLLING_INTERVAL;
    }, 60000); // 1분 동안 활동 없으면 비활성으로 간주
  }
  
  // 사용자 활동 이벤트 리스너
  document.addEventListener('mousemove', resetInactivityTimer);
  document.addEventListener('keydown', resetInactivityTimer);
  document.addEventListener('click', resetInactivityTimer);
  
  // 페이지 가시성 변경 처리
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      // 페이지가 숨겨지면 비활성으로 간주
      isUserActive = false;
      pollingInterval = INACTIVE_POLLING_INTERVAL;
    } else {
      // 페이지가 다시 보이면 활성으로 간주
      isUserActive = true;
      pollingInterval = ACTIVE_POLLING_INTERVAL;
      
      // 빠른 폴링으로 즉시 전환
      if (pollingTimeout) {
        clearTimeout(pollingTimeout);
        poll();
      }
    }
  });
  
  // 폴링 함수
  function poll() {
    fetch('/api/status')
      .then(response => response.json())
      .then(data => {
        console.log('폴링 데이터:', data);
        
        // 현재 활동 상태에 따른 간격으로 다음 폴링 예약
        pollingTimeout = setTimeout(poll, pollingInterval);
      })
      .catch(error => {
        console.error('폴링 오류:', error);
        
        // 오류 시에도 다음 폴링 예약
        pollingTimeout = setTimeout(poll, pollingInterval);
      });
  }
  
  // 초기 비활성 타이머 설정
  resetInactivityTimer();
  
  // 첫 번째 폴링 시작
  poll();
  
  // 클린업 함수
  return function cleanup() {
    if (pollingTimeout) {
      clearTimeout(pollingTimeout);
    }
    if (inactivityTimeout) {
      clearTimeout(inactivityTimeout);
    }
    document.removeEventListener('mousemove', resetInactivityTimer);
    document.removeEventListener('keydown', resetInactivityTimer);
    document.removeEventListener('click', resetInactivityTimer);
  };
}

const cleanup = userActivityBasedPolling();
네트워크 상태 감지

네트워크 상태에 따라 폴링 빈도를 조정한다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 네트워크 상태 감지 기반 폴링 예시 (JavaScript)
function networkAwarePolling() {
  const NORMAL_POLLING_INTERVAL = 5000;   // 정상 연결 시 5초
  const SLOW_POLLING_INTERVAL = 15000;    // 느린 연결 시 15초
  const OFFLINE_RETRY_INTERVAL = 30000;   // 오프라인 시 30초
  
  let pollingInterval = NORMAL_POLLING_INTERVAL;
  let isOnline = navigator.onLine;
  let connectionType = 'unknown';
  let pollingTimeout;
  
  // 연결 유형 감지 (Network Information API 사용)
  if ('connection' in navigator) {
    connectionType = navigator.connection.effectiveType;
    
    // 연결 상태 변화 감지
    navigator.connection.addEventListener('change', updateConnectionStatus);
  }
  
  // 온라인/오프라인 상태 감지
  window.addEventListener('online', () => {
    isOnline = true;
    updatePollingInterval();
    
    // 연결 복구 시 즉시 폴링
    if (pollingTimeout) {
      clearTimeout(pollingTimeout);
      poll();
    }
  });
  
  window.addEventListener('offline', () => {
    isOnline = false;
    updatePollingInterval();
  });
  
  function updateConnectionStatus() {
    if (navigator.connection) {
      connectionType = navigator.connection.effectiveType;
      updatePollingInterval();
    }
  }
  
  function updatePollingInterval() {
    if (!isOnline) {
      pollingInterval = OFFLINE_RETRY_INTERVAL;
      console.log('오프라인 상태, 폴링 간격:', pollingInterval);
      return;
    }
    
    // 연결 유형에 따른 간격 설정
    switch (connectionType) {
      case 'slow-2g':
      case '2g':
        pollingInterval = SLOW_POLLING_INTERVAL;
        break;
      case '3g':
        pollingInterval = NORMAL_POLLING_INTERVAL;
        break;
      case '4g':
        pollingInterval = NORMAL_POLLING_INTERVAL;
        break;
      default:
        pollingInterval = NORMAL_POLLING_INTERVAL;
    }
    
    console.log(`네트워크 상태: ${connectionType}, 폴링 간격: ${pollingInterval}ms`);
  }
  
  function poll() {
    if (!isOnline) {
      console.log('오프라인 상태, 다음 폴링 예약');
      pollingTimeout = setTimeout(poll, pollingInterval);
      return;
    }
    
    const startTime = Date.now();
    
    fetch('/api/status')
      .then(response => response.json())
      .then(data => {
        console.log('폴링 데이터:', data);
        
        // 응답 시간 측정
        const responseTime = Date.now() - startTime;
        
        // 응답 시간에 따른 연결 품질 추정
        if (responseTime > 1000) {
          // 응답이 느리면 폴링 간격 증가
          pollingInterval = Math.min(pollingInterval * 1.5, SLOW_POLLING_INTERVAL);
        } else {
          // 응답이 빠르면 폴링 간격 감소
          pollingInterval = Math.max(pollingInterval * 0.8, NORMAL_POLLING_INTERVAL);
        }
        
        // 다음 폴링 예약
        pollingTimeout = setTimeout(poll, pollingInterval);
      })
      .catch(error => {
        console.error('폴링 오류:', error);
        
        // 오류 시 폴링 간격 증가
        pollingInterval = Math.min(pollingInterval * 2, OFFLINE_RETRY_INTERVAL);
        
        // 다음 폴링 예약
        pollingTimeout = setTimeout(poll, pollingInterval);
      });
  }
  
  // 초기 폴링 간격 설정
  updatePollingInterval();
  
  // 첫 번째 폴링 시작
  poll();
  
  // 클린업 함수
  return function cleanup() {
    if (pollingTimeout) {
      clearTimeout(pollingTimeout);
    }
    window.removeEventListener('online', updatePollingInterval);
    window.removeEventListener('offline', updatePollingInterval);
    if ('connection' in navigator) {
      navigator.connection.removeEventListener('change', updateConnectionStatus);
    }
  };
}

const cleanup = networkAwarePolling();

하이브리드 접근법

폴링의 단점을 보완하기 위해 여러 통신 패턴을 결합한 접근법을 사용할 수 있다.

폴링과 웹훅 결합

폴링을 기본으로 사용하되, 가능한 경우 웹훅을 통해 즉시 알림을 받도록 한다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// 폴링과 웹훅 결합 예시 (JavaScript)
function hybridPollingWithWebhook() {
  const FALLBACK_POLLING_INTERVAL = 30000; // 폴백 폴링 간격: 30초
  
  let lastEventId = null;
  let pollingTimeout = null;
  let webhookConnected = false;
  
  // 웹훅 설정 (가능할 경우)
  async function setupWebhook() {
    try {
      // 웹훅 연결 요청
      const response = await fetch('/api/webhooks/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          callbackUrl: generateCallbackUrl(),
          events: ['data_updated', 'status_changed']
        })
      });
      
      if (response.ok) {
        const data = await response.json();
        console.log('웹훅 등록 성공:', data);
        webhookConnected = true;
        
        // 웹훅 연결 확인을 위한 주기적 확인
        startWebhookHeartbeat();
      } else {
        console.warn('웹훅 등록 실패, 폴링 사용');
        webhookConnected = false;
        startPolling();
      }
    } catch (error) {
      console.error('웹훅 설정 오류:', error);
      webhookConnected = false;
      startPolling();
    }
  }
  
  // 웹훅 콜백 URL 생성
  function generateCallbackUrl() {
    // 실제 구현에서는 서버나 서비스 워커로 웹훅을 수신할 적절한 URL 사용
    return 'https://webhook.example.com/callbacks/' + generateUniqueId();
  }
  
  // 고유 ID 생성
  function generateUniqueId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
  }
  
  // 웹훅 연결 상태 확인
  function startWebhookHeartbeat() {
    setInterval(async () => {
      try {
        const response = await fetch('/api/webhooks/status');
        const data = await response.json();
        
        if (!data.connected) {
          console.warn('웹훅 연결 끊김, 폴링으로 전환');
          webhookConnected = false;
          startPolling();
        }
      } catch (error) {
        console.error('웹훅 상태 확인 오류:', error);
        webhookConnected = false;
        startPolling();
      }
    }, 60000); // 1분마다 확인
  }
  
  // 폴링 시작
  function startPolling() {
    if (pollingTimeout) return; // 이미 폴링 중인 경우
    
    poll();
  }
  
  // 폴링 중지
  function stopPolling() {
    if (pollingTimeout) {
      clearTimeout(pollingTimeout);
      pollingTimeout = null;
    }
  }
  
  // 폴링 함수
  async function poll() {
    try {
      // 마지막 이벤트 ID가 있으면 그 이후의 이벤트만 요청
      const url = lastEventId 
        ? `/api/events?afterId=${lastEventId}`
        : '/api/events';
      
      const response = await fetch(url);
      const events = await response.json();
      
      if (events.length > 0) {
        // 이벤트 처리
        processEvents(events);
        
        // 마지막 이벤트 ID 업데이트
        lastEventId = events[events.length - 1].id;
      }
      
      // 웹훅이 연결되어 있지 않은 경우에만 계속 폴링
      if (!webhookConnected) {
        pollingTimeout = setTimeout(poll, FALLBACK_POLLING_INTERVAL);
      } else {
        stopPolling();
      }
    } catch (error) {
      console.error('폴링 오류:', error);
      
      // 오류 시에도 계속 폴링
      pollingTimeout = setTimeout(poll, FALLBACK_POLLING_INTERVAL);
    }
  }
  
  // 이벤트 처리
  function processEvents(events) {
    for (const event of events) {
      console.log('이벤트 처리:', event);
      // 이벤트 유형에 따른 처리
      switch (event.type) {
        case 'data_updated':
          updateData(event.data);
          break;
        case 'status_changed':
          updateStatus(event.data);
          break;
        default:
          console.log('알 수 없는 이벤트 유형:', event.type);
      }
    }
  }
  
  // 데이터 업데이트 처리
  function updateData(data) {
    console.log('데이터 업데이트:', data);
    // 데이터 업데이트 로직
  }
  
  // 상태 변경 처리
  function updateStatus(status) {
    console.log('상태 변경:', status);
    // 상태 변경 로직
  }
  
  // 웹훅 콜백 처리 (실제 구현에서는 서버나 서비스 워커에서 처리)
  function handleWebhookCallback(eventData) {
    console.log('웹훅 콜백 수신:', eventData);
    
    // 이벤트 처리
    processEvents([eventData]);
    
    // 이벤트 ID 업데이트
    if (eventData.id) {
      lastEventId = eventData.id;
    }
  }
  
  // 초기화
  setupWebhook();
  
  // API 노출
  return {
    refresh: poll,
    setWebhookStatus: (connected) => {
      webhookConnected = connected;
      if (!connected) {
        startPolling();
      } else {
        stopPolling();
      }
    },
    handleWebhookCallback
  };
}

const hybridClient = hybridPollingWithWebhook();
폴링과 서버-전송 이벤트(SSE) 결합

SSE를 주요 통신 채널로 사용하고, 폴링을 폴백 메커니즘으로 활용한다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// 폴링과 SSE 결합 예시 (JavaScript)
function hybridPollingWithSSE() {
  const FALLBACK_POLLING_INTERVAL = 30000; // 폴백 폴링 간격: 30초
  const SSE_RETRY_TIMEOUT = 10000;         // SSE 재연결 타임아웃: 10초
  
  let lastEventId = null;
  let pollingTimeout = null;
  let eventSource = null;
  let sseConnected = false;
  let sseRetryCount = 0;
  const MAX_SSE_RETRIES = 3;
  
  // SSE 연결 설정
  function setupSSE() {
    if (eventSource) {
      eventSource.close();
    }
    
    const url = lastEventId 
      ? `/api/events/stream?lastEventId=${lastEventId}`
      : '/api/events/stream';
    
    try {
      eventSource = new EventSource(url);
      
      // 연결 이벤트
      eventSource.onopen = function() {
        console.log('SSE 연결 성공');
        sseConnected = true;
        sseRetryCount = 0;
        stopPolling(); // SSE 연결 시 폴링 중지
      };
      
      // 메시지 이벤트
      eventSource.onmessage = function(event) {
        try {
          const data = JSON.parse(event.data);
          processEvent(data);
          
          // 이벤트 ID 업데이트
          if (data.id) {
            lastEventId = data.id;
          }
        } catch (error) {
          console.error('SSE 메시지 처리 오류:', error);
        }
      };
      
      // 특정 이벤트 유형 리스너
      eventSource.addEventListener('data_updated', function(event) {
        try {
          const data = JSON.parse(event.data);
          updateData(data);
        } catch (error) {
          console.error('데이터 업데이트 이벤트 처리 오류:', error);
        }
      });
      
      eventSource.addEventListener('status_changed', function(event) {
        try {
          const data = JSON.parse(event.data);
          updateStatus(data);
        } catch (error) {
          console.error('상태 변경 이벤트 처리 오류:', error);
        }
      });
      
      // 오류 이벤트
      eventSource.onerror = function(error) {
        console.error('SSE 오류:', error);
        sseConnected = false;
        
        // 재시도 횟수 확인
        if (sseRetryCount < MAX_SSE_RETRIES) {
          sseRetryCount++;
          console.log(`SSE 재연결 시도 (${sseRetryCount}/${MAX_SSE_RETRIES})...`);
          
          // 일정 시간 후 재연결 시도
          setTimeout(setupSSE, SSE_RETRY_TIMEOUT);
        } else {
          console.warn('최대 SSE 재시도 횟수 초과, 폴링으로 전환');
          eventSource.close();
          eventSource = null;
          startPolling();
        }
      };
    } catch (error) {
      console.error('SSE 설정 오류:', error);
      sseConnected = false;
      startPolling();
    }
  }
  
  // 폴링 시작
  function startPolling() {
    if (pollingTimeout) return; // 이미 폴링 중인 경우
    
    poll();
  }
  
  // 폴링 중지
  function stopPolling() {
    if (pollingTimeout) {
      clearTimeout(pollingTimeout);
      pollingTimeout = null;
    }
  }
  
  // 폴링 함수
  async function poll() {
    try {
      // 마지막 이벤트 ID가 있으면 그 이후의 이벤트만 요청
      const url = lastEventId 
        ? `/api/events?afterId=${lastEventId}`
        : '/api/events';
      
      const response = await fetch(url);
      const events = await response.json();
      
      if (events.length > 0) {
        // 이벤트 처리
        for (const event of events) {
          processEvent(event);
        }
        
        // 마지막 이벤트 ID 업데이트
        lastEventId = events[events.length - 1].id;
      }
      
      // SSE가 연결되어 있지 않은 경우에만 계속 폴링
      if (!sseConnected) {
        pollingTimeout = setTimeout(poll, FALLBACK_POLLING_INTERVAL);
      } else {
        stopPolling();
      }
    } catch (error) {
      console.error('폴링 오류:', error);
      
      // 오류 시에도 계속 폴링
      pollingTimeout = setTimeout(poll, FALLBACK_POLLING_INTERVAL);
    }
  }
  
  // 이벤트 처리
  function processEvent(event) {
    console.log('이벤트 처리:', event);
    
    // 이벤트 유형에 따른 처리
    switch (event.type) {
      case 'data_updated':
        updateData(event.data);
        break;
      case 'status_changed':
        updateStatus(event.data);
        break;
      default:
        console.log('알 수 없는 이벤트 유형:', event.type);
    }
  }
  
  // 데이터 업데이트 처리
  function updateData(data) {
    console.log('데이터 업데이트:', data);
    // 데이터 업데이트 로직
  }
  
  // 상태 변경 처리
  function updateStatus(status) {
    console.log('상태 변경:', status);
    // 상태 변경 로직
  }
  
  // 브라우저 SSE 지원 확인
  if ('EventSource' in window) {
    // SSE 지원, SSE 연결 시도
    setupSSE();
  } else {
    // SSE 미지원, 폴링 사용
    console.warn('브라우저가 Server-Sent Events를 지원하지 않음, 폴링으로 전환');
    startPolling();
  }
  
  // 페이지 가시성 변경 처리
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      // 페이지가 숨겨진 경우
      if (eventSource) {
        console.log('페이지 숨겨짐, SSE 연결 일시 중단');
        eventSource.close();
        eventSource = null;
        sseConnected = false;
      }
      stopPolling();
    } else {
      // 페이지가 다시 보이는 경우
      if (!eventSource && !pollingTimeout) {
        console.log('페이지 다시 표시됨, 데이터 연결 재개');
        if ('EventSource' in window) {
          setupSSE();
        } else {
          startPolling();
        }
      }
    }
  });
  
  // 클린업 함수
  return function cleanup() {
    if (eventSource) {
      eventSource.close();
    }
    stopPolling();
  };
}

const cleanup = hybridPollingWithSSE();

실제 사용 사례 및 구현 예시

비동기 작업 상태 확인

오래 걸리는 작업의 상태를 확인하기 위한 폴링 구현 예시.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 비동기 작업 상태 확인을 위한 폴링 클라이언트 (TypeScript)
interface Job {
  id: string;
  status: 'pending' | 'processing' | 'completed' | 'failed';
  progress: number;
  result?: any;
  error?: string;
}

class JobPollingClient {
  private readonly baseUrl: string;
  private readonly initialPollingInterval: number;
  private readonly maxPollingInterval: number;
  private readonly progressCallback?: (job: Job) => void;
  
  constructor(options: {
    baseUrl: string;
    initialPollingInterval?: number;
    maxPollingInterval?: number;
    progressCallback?: (job: Job) => void;
  }) {
    this.baseUrl = options.baseUrl;
    this.initialPollingInterval = options.initialPollingInterval || 2000;
    this.maxPollingInterval = options.maxPollingInterval || 30000;
    this.progressCallback = options.progressCallback;
  }
  
  /**
   * 작업을 시작하고 완료될 때까지 폴링
   */
  async startJobAndWaitForCompletion<T = any>(
    endpoint: string,
    payload: any
  ): Promise<T> {
    // 작업 시작
    const jobId = await this.startJob(endpoint, payload);
    
    // 작업 완료 대기
    return this.waitForJobCompletion<T>(jobId);
  }
  
  /**
   * 작업 시작
   */
  private async startJob(endpoint: string, payload: any): Promise<string> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });
    
    if (!response.ok) {
      throw new Error(`Failed to start job: ${response.status} ${response.statusText}`);
    }
    
    const data = await response.json();
    return data.jobId;
  }
  
  /**
   * 작업 완료 대기
   */
  async waitForJobCompletion<T = any>(jobId: string): Promise<T> {
    let pollingInterval = this.initialPollingInterval;
    
    while (true) {
      // 작업 상태 조회
      const job = await this.getJobStatus(jobId);
      
      // 진행 상황 콜백 호출
      if (this.progressCallback) {
        this.progressCallback(job);
      }
      
      // 작업 완료 확인
      if (job.status === 'completed') {
        return job.result as T;
      }
      
      // 작업 실패 확인
      if (job.status === 'failed') {
        throw new Error(`Job failed: ${job.error || 'Unknown error'}`);
      }
      
      // 폴링 간격 조정 (진행률에 따라)
      if (job.progress > 0) {
        // 진행률이 높을수록 폴링 간격 감소
        const progressFactor = 1 - (job.progress / 100);
        pollingInterval = Math.max(
          this.initialPollingInterval,
          Math.min(this.maxPollingInterval, 
                 this.initialPollingInterval + (progressFactor * (this.maxPollingInterval - this.initialPollingInterval)))
        );
      }
      
      // 대기
      await new Promise(resolve => setTimeout(resolve, pollingInterval));
    }
  }
  
  /**
   * 작업 상태 조회
   */
  private async getJobStatus(jobId: string): Promise<Job> {
    const response = await fetch(`${this.baseUrl}/jobs/${jobId}`);
    
    if (!response.ok) {
      throw new Error(`Failed to get job status: ${response.status} ${response.statusText}`);
    }
    
    return await response.json();
  }
  
  /**
   * 작업 취소
   */
  async cancelJob(jobId: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/jobs/${jobId}`, {
      method: 'DELETE'
    });
    
    if (!response.ok) {
      throw new Error(`Failed to cancel job: ${response.status} ${response.statusText}`);
    }
  }
}

// 사용 예시
async function processLargeFile() {
  const client = new JobPollingClient({
    baseUrl: 'https://api.example.com',
    progressCallback: (job) => {
      console.log(`처리 중: ${job.progress}%`);
      updateProgressBar(job.progress);
    }
  });
  
  try {
    const result = await client.startJobAndWaitForCompletion('/api/process-file', {
      fileId: '12345',
      options: {
        format: 'pdf',
        quality: 'high'
      }
    });
    
    console.log('파일 처리 완료:', result);
    showSuccessMessage('파일이 성공적으로 처리되었습니다.');
    
    return result;
  } catch (error) {
    console.error('파일 처리 실패:', error);
    showErrorMessage(`파일 처리 중 오류가 발생했습니다: ${error.message}`);
    throw error;
  }
}

function updateProgressBar(progress: number) {
  // 진행 표시줄 업데이트 로직
  document.getElementById('progress-bar').style.width = `${progress}%`;
  document.getElementById('progress-text').textContent = `${Math.round(progress)}%`;
}

function showSuccessMessage(message: string) {
  // 성공 메시지 표시 로직
}

function showErrorMessage(message: string) {
  // 오류 메시지 표시 로직
}

실시간 대시보드 업데이트

실시간 데이터를 표시하는 대시보드를 위한 폴링 구현 예시.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// 실시간 대시보드 업데이트 폴링 클라이언트 (TypeScript)
interface DashboardData {
  metrics: {
    [key: string]: number;
  };
  alerts: Array<{
    id: string;
    severity: 'info' | 'warning' | 'critical';
    message: string;
    timestamp: string;
  }>;
  activeUsers: number;
  lastUpdated: string;
  version: string;
}

class DashboardPoller {
  private readonly url: string;
  private readonly updateCallback: (data: DashboardData) => void;
  private readonly errorCallback?: (error: Error) => void;
  
  private pollingInterval: number = 10000; // 10초 기본 간격
  private lastVersion: string | null = null;
  private active: boolean = false;
  private timeoutId: number | null = null;
  private consecutiveErrors: number = 0;
  
  constructor(options: {
    url: string;
    updateCallback: (data: DashboardData) => void;
    errorCallback?: (error: Error) => void;
    initialPollingInterval?: number;
  }) {
    this.url = options.url;
    this.updateCallback = options.updateCallback;
    this.errorCallback = options.errorCallback;
    this.pollingInterval = options.initialPollingInterval || this.pollingInterval;
  }
  
  /**
   * 폴링 시작
   */
  start() {
    if (this.active) return;
    
    this.active = true;
    this.poll();
    
    // 페이지 가시성 변경 감지
    document.addEventListener('visibilitychange', this.handleVisibilityChange);
    
    console.log(`대시보드 폴링 시작: ${this.pollingInterval}ms 간격`);
  }
  
  /**
   * 폴링 중지
   */
  stop() {
    if (!this.active) return;
    
    this.active = false;
    
    if (this.timeoutId !== null) {
      clearTimeout(this.timeoutId);
      this.timeoutId = null;
    }
    
    document.removeEventListener('visibilitychange', this.handleVisibilityChange);
    
    console.log('대시보드 폴링 중지');
  }
  
  /**
   * 폴링 간격 변경
   */
  setPollingInterval(interval: number) {
    this.pollingInterval = interval;
    
    // 현재 활성 중이면 다음 폴링에 새 간격 적용
    if (this.active && this.timeoutId !== null) {
      clearTimeout(this.timeoutId);
      this.timeoutId = setTimeout(() => this.poll(), this.pollingInterval);
    }
    
    console.log(`폴링 간격 변경: ${this.pollingInterval}ms`);
  }
  
  /**
   * 폴링 실행
   */
  private async poll() {
    if (!this.active) return;
    
    try {
      // URL 생성 (조건부 요청)
      let url = this.url;
      if (this.lastVersion) {
        url += (url.includes('?') ? '&' : '?') + `version=${this.lastVersion}`;
      }
      
      // 대시보드 데이터 요청
      const response = await fetch(url, {
        headers: {
          'Accept': 'application/json',
          // 조건부 요청을 위한 헤더
          (this.lastVersion ? { 'If-None-Match': this.lastVersion } : {})
        }
      });
      
      // 변경 사항이 없는 경우 (304 Not Modified)
      if (response.status === 304) {
        console.log('대시보드 데이터 변경 없음');
        this.consecutiveErrors = 0;
        this.scheduleNextPoll();
        return;
      }
      
      // 오류 응답
      if (!response.ok) {
        throw new Error(`대시보드 데이터 가져오기 실패: ${response.status} ${response.statusText}`);
      }
      
      // 데이터 파싱
      const data: DashboardData = await response.json();
      
      // 마지막 버전 업데이트
      this.lastVersion = data.version;
      
      // 데이터 처리 및 콜백 호출
      this.updateCallback(data);
      
      // 경고 수에 따른 폴링 간격 조정
      const criticalAlerts = data.alerts.filter(a => a.severity === 'critical').length;
      if (criticalAlerts > 0) {
        // 심각한 경고가 있으면 폴링 간격 감소
        this.setPollingInterval(Math.max(2000, this.pollingInterval / 2));
      } else if (data.alerts.length === 0 && this.pollingInterval < 10000) {
        // 경고가 없으면 점진적으로 간격 증가
        this.setPollingInterval(Math.min(10000, this.pollingInterval * 1.5));
      }
      
      // 연속 오류 카운터 초기화
      this.consecutiveErrors = 0;
    } catch (error) {
      console.error('폴링 오류:', error);
      
      // 오류 콜백 호출
      if (this.errorCallback) {
        this.errorCallback(error);
      }
      
      // 연속 오류 카운트
      this.consecutiveErrors++;
      
      // 연속 오류에 따른 간격 증가 (지수 백오프)
      if (this.consecutiveErrors > 1) {
        const backoffFactor = Math.min(5, this.consecutiveErrors); // 최대 5배까지 증가
        this.setPollingInterval(Math.min(60000, this.pollingInterval * backoffFactor));
      }
    } finally {
      // 다음 폴링 예약
      this.scheduleNextPoll();
    }
  }
  
  /**
   * 다음 폴링 일정 설정
   */
  private scheduleNextPoll() {
    if (!this.active) return;
    
    this.timeoutId = setTimeout(() => this.poll(), this.pollingInterval);
  }
  
  /**
   * 페이지 가시성 변경 처리
   */
  private handleVisibilityChange = () => {
    if (document.hidden) {
      // 페이지가 숨겨진 경우, 폴링 간격 증가
      this.setPollingInterval(Math.min(60000, this.pollingInterval * 3));
    } else {
      // 페이지가 다시 보이는 경우, 즉시 폴링 및 간격 초기화
      if (this.timeoutId !== null) {
        clearTimeout(this.timeoutId);
      }
      this.setPollingInterval(10000);
      this.poll();
    }
  }
  
  /**
   * 즉시 새로고침
   */
  refresh() {
    if (this.timeoutId !== null) {
      clearTimeout(this.timeoutId);
      this.timeoutId = null;
    }
    
    this.poll();
  }
}

// 사용 예시
document.addEventListener('DOMContentLoaded', () => {
  const dashboardPoller = new DashboardPoller({
    url: '/api/dashboard',
    updateCallback: (data) => {
      // 대시보드 UI 업데이트
      updateMetricsDisplay(data.metrics);
      updateAlertsPanel(data.alerts);
      updateActiveUsersCounter(data.activeUsers);
      updateLastUpdatedTime(data.lastUpdated);
    },
    errorCallback: (error) => {
      showErrorNotification(`대시보드 업데이트 실패: ${error.message}`);
    },
    initialPollingInterval: 5000 // 첫 시작은 5초 간격
  });
  
  // 폴링 시작
  dashboardPoller.start();
  
  // 새로고침 버튼 이벤트 리스너
  document.getElementById('refresh-button')?.addEventListener('click', () => {
    dashboardPoller.refresh();
  });
  
  // 페이지 언로드 시 정리
  window.addEventListener('beforeunload', () => {
    dashboardPoller.stop();
  });
});

// UI 업데이트 함수들
function updateMetricsDisplay(metrics: Record<string, number>) {
  // 대시보드 지표 업데이트 로직
  Object.entries(metrics).forEach(([key, value]) => {
    const element = document.getElementById(`metric-${key}`);
    if (element) {
      element.textContent = value.toString();
      
      // 값 변경 시 시각적 피드백
      element.classList.add('updated');
      setTimeout(() => {
        element.classList.remove('updated');
      }, 2000);
    }
  });
}

function updateAlertsPanel(alerts: Array<any>) {
  // 알림 패널 업데이트 로직
  const alertsContainer = document.getElementById('alerts-container');
  if (!alertsContainer) return;
  
  // 기존 알림과 비교하여 새 알림만 추가
  const existingAlertIds = new Set(
    Array.from(alertsContainer.querySelectorAll('.alert'))
      .map(el => el.getAttribute('data-alert-id'))
  );
  
  // 새 알림 추가
  alerts.forEach(alert => {
    if (!existingAlertIds.has(alert.id)) {
      const alertElement = createAlertElement(alert);
      alertsContainer.prepend(alertElement);
      
      // 애니메이션 효과
      setTimeout(() => {
        alertElement.classList.add('visible');
      }, 10);
    }
  });
  
  // 더 이상 존재하지 않는 알림 제거
  const currentAlertIds = new Set(alerts.map(a => a.id));
  Array.from(alertsContainer.querySelectorAll('.alert'))
    .forEach(el => {
      const id = el.getAttribute('data-alert-id');
      if (id && !currentAlertIds.has(id)) {
        el.classList.remove('visible');
        setTimeout(() => {
          el.remove();
        }, 500);
      }
    });
}

function createAlertElement(alert: any) {
  const alertElement = document.createElement('div');
  alertElement.className = `alert alert-${alert.severity}`;
  alertElement.setAttribute('data-alert-id', alert.id);
  alertElement.innerHTML = `
    <div class="alert-time">${formatTime(alert.timestamp)}</div>
    <div class="alert-message">${alert.message}</div>
  `;
  return alertElement;
}

function updateActiveUsersCounter(count: number) {
  // 활성 사용자 수 업데이트 로직
  const element = document.getElementById('active-users-count');
  if (element) {
    element.textContent = count.toString();
  }
}

function updateLastUpdatedTime(timestamp: string) {
  // 최종 업데이트 시간 표시 로직
  const element = document.getElementById('last-updated');
  if (element) {
    element.textContent = `마지막 업데이트: ${formatTime(timestamp)}`;
  }
}

function formatTime(timestamp: string) {
  // 타임스탬프 형식화 로직
  return new Date(timestamp).toLocaleTimeString();
}

function showErrorNotification(message: string) {
  // 오류 알림 표시 로직
  const notification = document.createElement('div');
  notification.className = 'error-notification';
  notification.textContent = message;
  document.body.appendChild(notification);
  
  setTimeout(() => {
    notification.classList.add('show');
  }, 10);
  
  setTimeout(() => {
    notification.classList.remove('show');
    setTimeout(() => {
      notification.remove();
    }, 500);
  }, 5000);
}

이벤트 스트림 롱 폴링

여러 이벤트를 효율적으로 수신하기 위한 롱 폴링 구현 예시.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// 이벤트 스트림 롱 폴링 클라이언트 (TypeScript)
interface Event {
  id: string;
  type: string;
  data: any;
  timestamp: string;
}

type EventHandler = (event: Event) => void;

class EventStreamClient {
  private readonly baseUrl: string;
  private readonly eventHandlers: Map<string, EventHandler[]> = new Map();
  private readonly defaultHandler: EventHandler | null = null;
  
  private lastEventId: string | null = null;
  private active: boolean = false;
  private connecting: boolean = false;
  private retryCount: number = 0;
  private retryTimeout: number | null = null;
  private pollTimeout: number | null = null;
  
  constructor(options: {
    baseUrl: string;
    defaultHandler?: EventHandler;
  }) {
    this.baseUrl = options.baseUrl;
    this.defaultHandler = options.defaultHandler || null;
  }
  
  /**
   * 특정 이벤트 유형에 대한 핸들러 등록
   */
  on(eventType: string, handler: EventHandler) {
    if (!this.eventHandlers.has(eventType)) {
      this.eventHandlers.set(eventType, []);
    }
    
    this.eventHandlers.get(eventType)!.push(handler);
    return this;
  }
  
  /**
   * 특정 이벤트 유형에 대한 핸들러 제거
   */
  off(eventType: string, handler?: EventHandler) {
    if (!this.eventHandlers.has(eventType)) return this;
    
    if (!handler) {
      // 해당 이벤트 유형의 모든 핸들러 제거
      this.eventHandlers.delete(eventType);
    } else {
      // 특정 핸들러만 제거
      const handlers = this.eventHandlers.get(eventType)!;
      const index = handlers.indexOf(handler);
      if (index !== -1) {
        handlers.splice(index, 1);
      }
      
      if (handlers.length === 0) {
        this.eventHandlers.delete(eventType);
      }
    }
    
    return this;
  }
  
  /**
   * 모든 이벤트 핸들러 제거
   */
  offAll() {
    this.eventHandlers.clear();
    return this;
  }
  
  /**
   * 이벤트 스트림 연결 시작
   */
  connect() {
    if (this.active) return this;
    
    this.active = true;
    this.poll();
    
    return this;
  }
  
  /**
   * 이벤트 스트림 연결 종료
   */
  disconnect() {
    this.active = false;
    
    if (this.pollTimeout !== null) {
      clearTimeout(this.pollTimeout);
      this.pollTimeout = null;
    }
    
    if (this.retryTimeout !== null) {
      clearTimeout(this.retryTimeout);
      this.retryTimeout = null;
    }
    
    this.connecting = false;
    
    return this;
  }
  
  /**
   * 롱 폴링 요청 발송
   */
  private async poll() {
    if (!this.active || this.connecting) return;
    
    this.connecting = true;
    
    try {
      // 요청 URL 생성
      let url = `${this.baseUrl}/events/poll`;
      
      // 마지막 이벤트 ID가 있으면 추가
      if (this.lastEventId) {
        url += `?lastEventId=${encodeURIComponent(this.lastEventId)}`;
      }
      
      // 롱 폴링 요청 시작
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Accept': 'application/json',
          'Cache-Control': 'no-cache'
        },
        // 30초 타임아웃 (서버는 이보다 짧은 시간에 응답해야 함)
        signal: AbortSignal.timeout(30000)
      });
      
      if (!response.ok) {
        throw new Error(`Event stream request failed: ${response.status} ${response.statusText}`);
      }
      
      // 이벤트 데이터 파싱
      const events: Event[] = await response.json();
      
      // 이벤트 처리
      if (events.length > 0) {
        // 마지막 이벤트 ID 업데이트
        this.lastEventId = events[events.length - 1].id;
        
        // 각 이벤트 처리
        events.forEach(event => this.processEvent(event));
      }
      
      // 성공적인 응답, 재시도 카운터 초기화
      this.retryCount = 0;
      
      // 즉시 다음 폴링 시작
      this.connecting = false;
      this.poll();
    } catch (error) {
      console.error('Event stream error:', error);
      
      // 재연결 시도
      this.connecting = false;
      this.scheduleRetry();
    }
  }
  
  /**
   * 이벤트 처리
   */
  private processEvent(event: Event) {
    // 이벤트 타입에 등록된 핸들러 호출
    if (this.eventHandlers.has(event.type)) {
      this.eventHandlers.get(event.type)!.forEach(handler => {
        try {
          handler(event);
        } catch (error) {
          console.error(`Error in event handler for type ${event.type}:`, error);
        }
      });
    }
    
    // 기본 핸들러 호출
    if (this.defaultHandler) {
      try {
        this.defaultHandler(event);
      } catch (error) {
        console.error('Error in default event handler:', error);
      }
    }
  }
  
  /**
   * 재연결 일정 설정
   */
  private scheduleRetry() {
    if (!this.active) return;
    
    // 지수 백오프 적용
    const delay = Math.min(30000, 1000 * Math.pow(2, this.retryCount));
    this.retryCount++;
    
    console.log(`Scheduling event stream retry in ${delay}ms (attempt ${this.retryCount})`);
    
    this.retryTimeout = setTimeout(() => {
      this.retryTimeout = null;
      this.poll();
    }, delay);
  }
}

// 사용 예시
document.addEventListener('DOMContentLoaded', () => {
  const eventStream = new EventStreamClient({
    baseUrl: '/api',
    defaultHandler: (event) => {
      console.log(`Received event: ${event.type}`, event);
    }
  });
  
  // 특정 이벤트 유형에 대한 핸들러 등록
  eventStream
    .on('user.login', (event) => {
      showNotification(`${event.data.username}님이 로그인했습니다.`);
      updateOnlineUsersList();
    })
    .on('message.new', (event) => {
      addMessageToChat(event.data);
      updateUnreadCount();
    })
    .on('notification', (event) => {
      showNotification(event.data.message);
    });
  
  // 페이지가 활성화될 때 연결
  if (!document.hidden) {
    eventStream.connect();
  }
  
  // 페이지 가시성 변경 처리
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      eventStream.disconnect();
    } else {
      eventStream.connect();
    }
  });
  
  // 페이지 언로드 시 정리
  window.addEventListener('beforeunload', () => {
    eventStream.disconnect();
  });
});

// UI 헬퍼 함수
function showNotification(message: string) {
  // 알림 표시 로직
  const notification = document.createElement('div');
  notification.className = 'notification';
  notification.textContent = message;
  document.getElementById('notifications-container')?.appendChild(notification);
  
  setTimeout(() => {
    notification.classList.add('show');
  }, 10);
  
  setTimeout(() => {
    notification.classList.remove('show');
    setTimeout(() => {
      notification.remove();
    }, 500);
  }, 5000);
}

function addMessageToChat(messageData: any) {
  // 채팅 메시지 추가 로직
  const chatContainer = document.getElementById('chat-messages');
  if (!chatContainer) return;
  
  const messageElement = document.createElement('div');
  messageElement.className = 'chat-message';
  messageElement.innerHTML = `
    <div class="message-sender">${messageData.sender}</div>
    <div class="message-content">${messageData.content}</div>
    <div class="message-time">${formatTime(messageData.timestamp)}</div>
  `;
  
  chatContainer.appendChild(messageElement);
  chatContainer.scrollTop = chatContainer.scrollHeight;
}

function updateOnlineUsersList() {
  // 온라인 사용자 목록 업데이트 로직
  fetch('/api/users/online')
    .then(response => response.json())
    .then(users => {
      const usersList = document.getElementById('online-users');
      if (!usersList) return;
      
      usersList.innerHTML = '';
      users.forEach((user: any) => {
        const userElement = document.createElement('div');
        userElement.className = 'user-item';
        userElement.innerHTML = `
          <div class="user-avatar" style="background-color: ${stringToColor(user.username)}">
            ${user.username.charAt(0).toUpperCase()}
          </div>
          <div class="user-name">${user.username}</div>
        `;
        usersList.appendChild(userElement);
      });
    })
    .catch(error => console.error('Failed to update online users list:', error));
}

function updateUnreadCount() {
  // 안 읽은 메시지 수 업데이트 로직
  const badge = document.getElementById('unread-badge');
  if (!badge) return;
  
  const currentCount = parseInt(badge.textContent || '0', 10);
  badge.textContent = (currentCount + 1).toString();
  badge.classList.add('pulse');
  
  setTimeout(() => {
    badge.classList.remove('pulse');
  }, 1000);
}

function formatTime(timestamp: string) {
  // 타임스탬프 형식화 로직
  return new Date(timestamp).toLocaleTimeString();
}

function stringToColor(str: string) {
  // 문자열을 색상으로 변환하는 로직
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  
  let color = '#';
  for (let i = 0; i < 3; i++) {
    const value = (hash >> (i * 8)) & 0xFF;
    color += ('00' + value.toString(16)).substr(-2);
  }
  
  return color;
}

폴링의 미래와 발전 방향

폴링은 오랫동안 API 통합의 기본 패턴으로 사용되어 왔으며, 앞으로도 진화를 계속할 것이다.

  1. 지능형 폴링 알고리즘
    인공지능과 기계학습을 활용한 고급 폴링 최적화 기법이 등장하고 있다.

    • 예측적 폴링(Predictive Polling): 과거 데이터 변경 패턴을 분석하여 최적의 폴링 시점 예측
    • 컨텍스트 인식 폴링(Context-Aware Polling): 사용자 행동, 시스템 상태, 네트워크 조건 등을 종합적으로 고려한 폴링 전략
    • 자가 최적화(Self-Optimizing) 알고리즘: 폴링 효율성을 실시간으로 분석하고 자동으로 전략 조정
  2. 저전력 장치를 위한 최적화
    IoT와 모바일 장치의 증가로 인해 배터리 효율성을 고려한 폴링 전략이 중요해지고 있다.

    • 배터리 인식 폴링(Battery-Aware Polling): 장치 배터리 수준에 따라 폴링 빈도 조정
    • 네트워크 효율적 폴링(Network-Efficient Polling): 데이터 사용량과 네트워크 비용을 최소화하는 전략
    • 푸시-폴 하이브리드(Push-Pull Hybrid): 가능한 경우 푸시 알림을 활용하고 필요할 때만 폴링으로 보완
  3. GraphQL 구독과의 통합
    GraphQL 구독과 폴링을 결합한 하이브리드 접근법이 점점 인기를 얻고 있다.

    • 폴백 메커니즘으로서의 폴링: WebSocket을 지원하지 않는 환경에서 GraphQL 구독의 대안으로 폴링 사용
    • 선택적 실시간성: 중요도에 따라 일부 업데이트는 구독으로, 덜 중요한 업데이트는 폴링으로 처리
    • 리소스 최적화 전략: 클라이언트와 서버 상태에 따라 구독과 폴링 간 동적 전환
  4. 서버리스 및 엣지 컴퓨팅과의 통합
    클라우드 네이티브 및 엣지 컴퓨팅 환경에서 폴링을 최적화하는 방법도 발전하고 있다.

    • 엣지 기반 폴링 최적화: 사용자에 가까운 엣지 노드에서 폴링 처리하여 지연 시간 감소
    • 서버리스 폴링 함수: 필요할 때만 활성화되는 서버리스 함수로 폴링 구현
    • 콜드 스타트 최적화: 서버리스 환경에서 폴링의 콜드 스타트 문제 해결 전략

용어 정리

용어설명

참고 및 출처