본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 19. 01:45

iPhone의 Apple Intelligence 에지 글로우(Edge Glow) 효과를 Mac에서 재현하기

요약

iPhone의 Apple Intelligence 에지 글로우 효과를 macOS 메뉴 바 앱으로 재현하는 과정을 다룹니다. Core Animation의 상태 손실 문제를 해결하기 위해 타이머 기반의 애니메이션 구현 방식을 제안합니다.

핵심 포인트

  • Apple Intelligence의 에지 글로우 효과를 macOS용 오픈 소스 앱으로 구현
  • CABasicAnimation 사용 시 창 가시성 변화에 따른 애니메이션 오류 발생 확인
  • 상태 손실 문제를 해결하기 위해 60fps 타이머 기반 애니메이션 방식 채택
  • CPU 사용량을 최소화하면서 견고한 애니메이션 효과 구현

iPhone의 Apple Intelligence는 Siri가 생각 중일 때 화면 가장자리에 아름다운 무지갯빛 글로우(iridescent glow)를 보여줍니다. 저는 Claude Code가 작동할 때 제 Mac에서도 동일한 효과를 보고 싶었습니다.

그래서 저는 EdgeGlow를 만들었습니다. 이는 iPhone의 에지 글로우(edge glow) 효과를 Mac에서 재현하는 무료 오픈 소스 macOS 메뉴 바 앱입니다.

이 포스트에서는 제가 이를 어떻게 구현했는지와 직면했던 기술적 과제들에 대해 설명하겠습니다.

작동 방식

AI Agent 트리거 훅(hook) → curl http://127.0.0.1:9876/start → 화면 글로우(glow) 발생
AI Agent 종료           → curl http://127.0.0.1:9876/stop  → 글로우(glow) 사라짐

그게 전부입니다. 매우 간단합니다.

흥미로운 부분은 애니메이션입니다. 그 과정을 안내해 드리겠습니다.

기술적 과제

화면 가장자리를 따라 부드러운 마키(marquee) 효과, 즉 본질적으로 지속적으로 흐르는 점선(dashed line)이 필요했습니다.

왜 CABasicAnimation을 사용하지 않았나?

저의 첫 번째 접근 방식은 lineDashPhaseCABasicAnimation을 사용하는 것이었습니다:

let anim = CABasicAnimation(keyPath: "lineDashPhase")
anim.fromValue = 0
anim.toValue = perimeter
...

이 방식은 작동했습니다... 창이 숨겨졌다가 다시 나타날 때까지는 말이죠.

Core Animation이 애니메이션 상태를 잃어버리게 되면, 마키(marquee) 효과는 다음과 같이 동작합니다:

  • 그 자리에 멈춤 (Freeze)
  • 갑자기 방향이 반전됨 (Reverse direction)
  • 무작위 위치로 튀어 오름 (Jump)

근본 원인

CABasicAnimation은 레이어의 프레젠테이션 상태(presentation state)와 연결된 Core Animation의 애니메이션 시스템에 의존합니다. 창이 숨겨지거나(orderOut) 레이어가 계층 구조에서 제거되면 애니메이션 상태가 손실됩니다.

저는 몇 가지 해결책을 시도했습니다:

  1. 애니메이션 일시정지/재개 (Pause/resume animations) — 안정적으로 작동하지 않았습니다.
  2. 창을 계속 활성화 상태로 유지 (Keep the window alive) — 리소스 낭비였습니다.
  3. 표시될 때 애니메이션 초기화 (Reset animation on show) — 시각적 점프(visual jumps)를 유발했습니다.

그 어떤 것도 완벽하지 않았습니다.

해결책: 타이머 기반 애니메이션 (Timer-Driven Animation)

저는 lineDashPhase를 직접 업데이트하는 60fps의 Timer로 전환했습니다:

private var flowTimer: Timer?
private var dashPhase: CGFloat = 0
private var lastTickTime: CFTimeInterval = 0
...

이 방식은 **매우 견고(bulletproof)**합니다. Core Animation의 애니메이션 시스템에 의존하지 않으며, 창의 가시성(visibility) 변화에 따른 상태 손실도 없습니다.

성능 또한 매우 뛰어납니다. 60fps로 단일 CGFloat 속성을 업데이트하는 데 CPU 사용량은 약 0%에 불과합니다.

4개 레이어 + 20개 세그먼트 글로우(Glow) 효과

iPhone의 Apple Intelligence 에지 글로우(edge glow)를 재현하기 위해 두 가지가 필요했습니다.

1. 20개 세그먼트 그라데이션 컬러링 (20-segment gradient coloring): 화면 가장자리를 20개의 세그먼트로 나누고, 각 세그먼트에 서로 다른 색조(보라색 → 파란색 → 청록색 → 분홍색 → 주황색 → 금색)를 적용합니다. 세그먼트 경계에 가우시안 블러 (Gaussian blur)를 적용하면 부드러운 전환이 만들어집니다:

for i in 0..<20 {
    let hue = (baseHue + Double(i) * 0.05).truncatingRemainder(dividingBy: 1.0)
    // 각 세그먼트는 서로 다른 색을 가지며, 그라데이션을 형성합니다
...

2. 4개 레이어 글로우 스택 (4-layer glow stack): 서로 다른 블러(blur) 레벨을 가진 4개의 CAShapeLayer 인스턴스를 쌓아 사실적인 네온 글로우 효과를 만듭니다:

레이어 구성 (Layer Configuration)

let configs: [(widthMul: CGFloat, alphaMul: CGFloat, blur: Double)] = [
    (baseWidth * 1.5, 0.15, 12.0),  // 레이어 1: 넓은 선, 높은 블러, 낮은 알파 → 외부 글로우 (outer glow)
    (baseWidth * 0.8, 0.30, 8.0),   // 레이어 2: 중간 선, 중간 블러, 중간 알파 → 중간 글로우 (mid glow)
...

각 레이어 구축 (Building Each Layer)

for (lineWidth, alpha, blur) in configs {
    let shape = CAShapeLayer()
    shape.frame = ringLayer.bounds
...

결과 (The Result)

4개의 레이어가 서로 겹쳐 쌓이면서 사실적인 네온 조명 튜브 효과를 만들어냅니다:

레이어 4: ██ (밝은 중심부, 블러 없음)
레이어 3: ████ (코어 라인, 블러 2)
레이어 2: ██████ (중간 글로우, 블러 8)
...

외부 레이어는 부드러운 글로우 헤일로(glow halo)를 형성하고, 내부 레이어는 밝은 코어를 형성합니다. 이들이 합쳐져 실제 네온 조명처럼 보이게 됩니다.

멀티 터미널 참조 횟수 계산 (Multi-Terminal Reference Counting)

문제점 (The Problem)

여러 개의 Claude Code 터미널이 실행 중일 때, 하나에서 /stop을 호출하면 다른 터미널들이 여전히 활성 상태임에도 불구하고 글로우 효과가 종료되어 버립니다.

해결책: 참조 횟수 계산 (Reference Counting)

class ControlServer {
    private var activeCount = 0

...

이제 /start는 카운트(count)를 증가시키고, /stop은 감소시킵니다. 글로우(glow) 효과는 카운트가 0에 도달했을 때만 숨겨집니다.

안전 타임아웃 (Safety Timeout)

만약 에이전트(agent)가 /stop을 보내지 않고 충돌(crash)한다면 어떻게 될까요? 참조 카운트(reference count)는 영원히 0보다 큰 상태로 유지될 것입니다.

해결책: 120초의 안전 타임아웃(safety timeout)을 설정합니다:

private func resetSafetyTimer() {
    safetyTimer?.cancel()
    let work = DispatchWorkItem { [weak self] in
...

모든 /start는 타이머를 재설정합니다. 60초 동안 /start가 수신되지 않으면 카운트는 0으로 재설정됩니다.

보안 고려 사항 (Security Considerations)

EdgeGlow는 127.0.0.1:9876에서 HTTP 서버를 실행합니다. 보안에 주의해야 했습니다:

로컬호스트 전용 (Localhost Only)

let param = NWParameters.tcp
param.acceptLocalOnly = true  // localhost로부터의 연결만 허용

이는 외부 네트워크 접근을 방지합니다.

GET 요청만 허용 (GET Requests Only)

guard method == "GET" || method == "OPTIONS" else {
    sendResponse(405, "Method Not Allowed", conn: conn)
    return
...

CSRF 공격을 방지하기 위해 POST/PUT/DELETE 요청을 거부합니다.

CORS 헤더 없음 (No CORS Headers)

// Access-Control-Allow-Origin 헤더 없음
// 웹 JavaScript가 엔드포인트(endpoints)를 호출할 수 없음

이는 웹 페이지가 JavaScript를 통해 API를 호출하는 것을 방지합니다.

데이터 수집 없음 (No Data Collection)

  • 분석(analytics) 없음
  • 텔레메트리(telemetry) 없음
  • 네트워크 요청 없음 (localhost 제외)

멀티 모니터 지원 (Multi-Monitor Support)

과제 (The Challenge)

크기가 서로 다른 여러 디스플레이를 어떻게 처리할 것인가?

해결책 (The Solution)

모든 화면 프레임(screen frames)의 합집합(union)을 계산합니다:

func totalScreenFrame() -> NSRect {
    var frame = NSRect.zero
    for screen in NSScreen.screens {
...

디스플레이 변경 처리 (Handling Display Changes)

화면 파라미터 변경을 감지합니다:

NotificationCenter.default.addObserver(
    forName: NSApplication.didChangeScreenParametersNotification,
    object: nil,
...

디스플레이가 변경되면 새로운 프레임 크기에 맞춰 레이어(layers)를 다시 구축합니다.

설정 지속성 (Settings Persistence)

과제 (The Challenge)

설정을 반응형(reactive)으로 만들고 앱 실행 간에 지속(persist)되게 하려면 어떻게 해야 하는가?

해결책 (The Solution)

ObservableObject + UserDefaults + Combine을 사용합니다:

class AppSettings: ObservableObject {
    @Published var speed: Int {
        didSet {
...

그 다음, GlowWindow에서 변경 사항을 관찰(observe)합니다:

settings.$speed.sink { [weak self] _ in
    self?.rebuildLayers()
}.store(in: &cancellables)

사용자가 속도 슬라이더(speed slider)를 변경하면, 레이어(layers)가 자동으로 재구성(rebuild)됩니다.

성능 (Performance)

지표 (Metric)값 (Value)비고 (Notes)
CPU~0%Timer 60fps가 단 하나의 CGFloat만 업데이트함
...

핵심 통찰(key insight): 60fps로 속성(property)을 업데이트하는 것은 비용이 적게 듭니다. 실제로 무거운 작업을 수행하는 것은 GPU 가속을 받는 CAShapeLayer 렌더링입니다.

체험하기 (Try It)

저의 첫 번째 macOS 앱입니다. 외부 라이브러리(third-party dependencies) 없이 순수 Swift + SwiftUI로 제작되었습니다.

여러분의 피드백을 기다립니다!

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0