← 모든 글

SwiftUI Layout 프로토콜: sizeThatFits부터 placeSubviews까지 커스텀 레이아웃 구축하기

iOS 16은 SwiftUI에 Layout 프로토콜을 추가했습니다. 이는 SwiftUI의 레이아웃 패스에 참여하는 커스텀 컨테이너 뷰를 구축하기 위한 공개 API입니다1. Layout 이전에는 커스텀 컨테이너 모양을 만들려면 GeometryReader 핵(전체 제안된 크기를 요청하기 때문에 컴포지션을 깨뜨리는 방식)이나 시스템과 싸우는 커스텀 ViewModifier 작업이 필요했습니다. Layout이 올바른 답입니다. 두 개의 메서드 프로토콜(sizeThatFitsplaceSubviews)에 선택적인 간격 및 캐싱 확장이 더해진 형태이며, SwiftUI의 부모-제안-자식-처리 레이아웃 모델과 깔끔하게 통합되는 계약을 가지고 있습니다.

이 글은 Apple의 문서를 따라 프로토콜을 살펴봅니다. 프레임은 “Layout이 실제로 어떤 것을 계약하는가”입니다. 왜냐하면 잘못된 사용 패턴(Layout을 크기 협상 도구가 아닌 좌표 공간 도구로 취급하는 것)은 한 화면에서는 동작하지만 다른 화면에서는 실패하는 레이아웃을 만들기 때문입니다. 그리고 클러스터의 What SwiftUI Is Made Of 포스트는 SwiftUI의 아키텍처를 가장 잘 이해하는 방법은 그것의 공개 프로토콜을 읽는 것이라고 주장했습니다.

TL;DR

  • Layout은 두 가지 필수 메서드를 가진 프로토콜입니다. sizeThatFits(proposal:subviews:cache:)는 부모의 제안이 주어졌을 때 레이아웃이 선호하는 크기를 반환하고, placeSubviews(in:proposal:subviews:cache:)는 각 자식의 place(at:anchor:proposal:) 메서드를 호출하여 자식을 배치합니다2.
  • proposal 매개변수는 ProposedViewSize이며 widthheight가 옵셔널 CGFloat입니다. nil은 “이상적인 크기를 사용하라”를 의미하고, 유한한 값은 부모의 제안이며, .infinity는 “원하는 만큼 사용하라”를 의미합니다.
  • SubviewsLayoutSubviews의 typealias이며, LayoutSubview 프록시의 컬렉션입니다. 각 프록시는 어떤 제안에 대해서든 크기를 질의할 수 있고 어떤 지점에든 배치될 수 있습니다. 프록시는 Layout이 자식과 상호 작용하는 유일한 방법입니다.
  • 커스텀 레이아웃 값은 자식 뷰에 .layoutValue(...)를 통해 첨부된 LayoutValueKey 타입을 통해 자식에서 부모로 흐르며, 레이아웃 메서드 내부에서 LayoutSubview 서브스크립트로 읽을 수 있습니다.
  • cachesizeThatFitsplaceSubviews 사이에서 계산을 분할 상환하기 위한 것입니다(각 패스가 둘 다 호출하며, 종종 동일한 중간 값을 사용합니다). 캐시를 미리 계산된 크기를 보유하는 구조체로 타입 지정하고, 한 번 만들어 두 메서드에서 재사용하세요.

프로토콜 계약

Layout은 (일반적으로) Apple의 프레임워크가 레이아웃 패스 동안 호출하는 두 가지 메서드를 선언하는 구조체입니다2:

struct DiagonalLayout: Layout {
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) -> CGSize {
        // Compute and return the size your layout wants
    }

    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) {
        // Position each subview by calling subview.place(...)
    }
}

내장 컨테이너처럼 사용하세요:

DiagonalLayout {
    Text("First")
    Text("Second")
    Text("Third")
}

프레임워크는 부모로부터 제안된 크기(ProposedViewSize)와 함께 sizeThatFits를 호출한 다음, 레이아웃에 부여된 경계와 함께 placeSubviews를 호출합니다. 두 메서드는 함께 레이아웃의 동작을 설명합니다. 즉, 얼마나 크기를 원하는지, 그리고 그 할당 내에서 각 자식이 어디로 가는지를 나타냅니다.

ProposedViewSize: 부모의 제안

SwiftUI의 레이아웃은 부모-제안-자식-처리 계약을 따릅니다3. 부모가 제안된 크기를 전달하면, 자식이 실제 크기를 반환하고, 부모는 자신의 경계 내에 자식을 배치합니다. LayoutProposedViewSize를 통해 이 계약에 참여합니다:

struct ProposedViewSize {
    var width: CGFloat?
    var height: CGFloat?
}

옵셔널 축은 의미론적 의미를 담고 있습니다:

  • 축에 대해 nil은 “이상적/자연적 크기를 사용하라”를 의미합니다. .zero가 제안된 Text는 최소 너비를 반환합니다(한 줄에 한 글자). nil이 제안되면 이상적인 너비를 반환합니다(한 줄, 줄 바꿈 없음).
  • 유한한 값은 “부모가 이만큼의 공간을 제공하니, 어떻게 할지 결정하라”를 의미합니다. 너비 100pt가 제안된 Text는 줄 바꿈할 수도 있고, 더 적게 사용할 수도 있고, 정확히 100을 사용할 수도 있습니다.
  • .infinity는 “원하는 만큼 사용하라”를 의미합니다. .infinity가 제안된 Color는 사용 가능한 전체 공간을 차지합니다.

관례상 ProposedViewSize.unspecified(width: nil, height: nil)는 이상적인 크기에 대한 요청이며, ProposedViewSize.zero는 최소 크기에 대한 요청이고, ProposedViewSize.infinity는 욕심스러운 확장에 대한 요청입니다.

커스텀 LayoutsizeThatFits는 제안을 존중해야 합니다. 항상 동일한 하드코딩된 값이 아니라, 제안된 경계에 대해 레이아웃이 실제로 원하는 크기를 반환해야 합니다. 하드코딩된 크기는 레이아웃이 다른 컨테이너(카드 뷰, 리스트 셀, 시트)에 적응하는 능력을 깨뜨립니다.

LayoutSubview를 통한 서브뷰 크기 읽기

sizeThatFits 내부에서 레이아웃은 다양한 제안에 대해 각 자식이 원하는 크기를 묻습니다. 질의는 LayoutSubview 프록시를 통해 진행됩니다4:

func sizeThatFits(
    proposal: ProposedViewSize,
    subviews: Subviews,
    cache: inout ()
) -> CGSize {
    let proposed = ProposedViewSize(
        width: proposal.width.map { $0 / CGFloat(subviews.count) },
        height: proposal.height
    )

    let sizes = subviews.map { $0.sizeThatFits(proposed) }
    let totalWidth = sizes.reduce(0) { $0 + $1.width }
    let maxHeight = sizes.map(\.height).max() ?? 0

    return CGSize(width: totalWidth, height: maxHeight)
}

subviews.map { $0.sizeThatFits(proposal) } 패턴은 레이아웃이 자식들이 원하는 크기를 발견하는 방법입니다. LayoutSubview 프록시의 sizeThatFits(_:) 메서드는 Layout 프로토콜 메서드와 동일하지 않습니다. 이는 제안이 주어졌을 때 자식의 선호 크기에 대한 프록시의 질의입니다. 둘은 동일한 협상에 참여하기 때문에 이름을 공유하지만, 계약의 다른 계층입니다.

자식들의 크기를 알고 싶은 레이아웃은 proxy.sizeThatFits(_:)를 호출합니다. 자식들을 배치하고 싶은 레이아웃은 placeSubviews 내부에서 proxy.place(at:anchor:proposal:)를 호출합니다.

서브뷰 배치하기

placeSubviews는 레이아웃이 위치 결정을 내리는 곳입니다2:

func placeSubviews(
    in bounds: CGRect,
    proposal: ProposedViewSize,
    subviews: Subviews,
    cache: inout ()
) {
    var x = bounds.minX
    let y = bounds.midY

    for subview in subviews {
        let size = subview.sizeThatFits(.unspecified)
        subview.place(
            at: CGPoint(x: x + size.width / 2, y: y),
            anchor: .center,
            proposal: ProposedViewSize(size)
        )
        x += size.width
    }
}

place(at:anchor:proposal:) 호출은 단일 서브뷰를 배치합니다. 세 가지 매개변수:

  • at: 부모의 좌표 공간에서의 위치.
  • anchor: 서브뷰의 어느 지점이 at에 위치하는가. .center는 서브뷰의 중심을 at에 두고, .topLeading은 좌상단 모서리를 그곳에 둡니다.
  • proposal: 서브뷰가 렌더링되어야 할 크기. 동일한 서브뷰의 sizeThatFits에서 반환된 크기를 전달하여 그 선호를 존중하거나, 커스텀 제안을 전달하여 제한할 수 있습니다.

모든 서브뷰는 placeSubviews 호출당 정확히 한 번 배치되어야 합니다. 서브뷰를 건너뛰면 배치되지 않은 채로 남게 됩니다(렌더링된 레이아웃에서 사라짐). 하나를 두 번 배치하는 것은 런타임 오류입니다.

LayoutValueKey를 통한 커스텀 레이아웃 값

자식이 부모 레이아웃에 무언가(우선순위, 스팬, 카테고리)를 전달해야 할 때, 채널은 LayoutValueKey입니다5:

struct PriorityKey: LayoutValueKey {
    static let defaultValue: Int = 0
}

extension View {
    func layoutPriority(_ value: Int) -> some View {
        layoutValue(key: PriorityKey.self, value: value)
    }
}

// Inside the Layout:
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
    let sortedSubviews = subviews.sorted {
        $0[PriorityKey.self] > $1[PriorityKey.self]
    }
    // ... place sortedSubviews
}

LayoutValueKey 프로토콜은 부모-자식 통신을 위한 타입화된 채널을 제공합니다. 자식은 layout-value 모디파이어를 통해 값을 첨부하고, 부모는 LayoutSubview 서브스크립트를 통해 그것을 읽습니다. 각 키는 명시적으로 지정하지 않은 서브뷰에 대한 기본값을 가집니다.

이 패턴은 개념적으로 .layoutPriority(_:) 같은 내장 모디파이어가 표현하는 것과 같습니다. 프레임워크는 그 특정 값을 공개 LayoutValueKey가 아닌 LayoutSubview의 전용 priority: Double 프로퍼티를 통해 노출하므로, 레이아웃 우선순위에 대한 프록시 접근은 키 서브스크립트가 아니라 subview.priority입니다. 커스텀 레이아웃은 자식들로부터 필요한 다른 구조화된 데이터를 위해 자체 LayoutValueKey 타입을 선언합니다.

cache 매개변수

두 레이아웃 메서드 모두 cache: inout 매개변수를 받습니다. 캐시는 레이아웃이 sizeThatFitsplaceSubviews 사이에서 작업을 분할 상환하는 곳입니다6:

struct DiagonalLayout: Layout {
    struct Cache {
        var sizes: [CGSize]
    }

    func makeCache(subviews: Subviews) -> Cache {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
        return Cache(sizes: sizes)
    }

    func updateCache(_ cache: inout Cache, subviews: Subviews) {
        cache.sizes = subviews.map { $0.sizeThatFits(.unspecified) }
    }

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
        let totalWidth = cache.sizes.reduce(0) { $0 + $1.width }
        let totalHeight = cache.sizes.reduce(0) { $0 + $1.height }
        return CGSize(width: totalWidth, height: totalHeight)
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
        var x = bounds.minX
        var y = bounds.minY
        for (subview, size) in zip(subviews, cache.sizes) {
            subview.place(
                at: CGPoint(x: x, y: y),
                anchor: .topLeading,
                proposal: ProposedViewSize(size)
            )
            x += size.width
            y += size.height
        }
    }
}

기본 cache 타입은 Void입니다. 대부분의 레이아웃은 캐시를 무시할 수 있으며, 크기 계산이 진정으로 비싸고(재귀적 측정, 동적 크기 결정) 동일한 중간 값이 두 레이아웃 메서드에 모두 공급될 때만 그 자리를 얻습니다.

makeCache(subviews:)는 레이아웃 패스당 한 번 실행되며, updateCache(_:subviews:)는 패스 사이에서 서브뷰가 변경될 때 실행됩니다. 이 패턴은 자식들 자체가 변경될 때 레이아웃이 캐시된 상태를 올바르게 무효화할 수 있게 합니다.

만들어 볼 만한 일반적인 커스텀 레이아웃

직접 만들어 볼 만한 세 가지 패턴:

Flow 레이아웃(아이템 줄 바꿈). 사용 가능한 너비를 초과하면 아이템이 여러 행으로 줄 바꿈됩니다. Apple의 HStack은 줄 바꿈하지 않습니다. 커스텀 Layout은 가능합니다. 각 자식을 측정하고, 왼쪽에서 오른쪽으로 배치하며, 행 너비가 제안의 너비를 초과하면 다음 행으로 진행합니다.

대각선 스택. 아이템이 대각선으로 스태거됩니다(각 자식이 이전 자식보다 약간 아래-오른쪽으로 위치함). 스택된 카드 UI, 갤러리 미리보기 레이아웃, 패럴랙스 느낌의 스택에 유용합니다.

파이/원형 레이아웃. 아이템이 원의 둘레를 따라 배치됩니다. 방사형 메뉴, 시간 기반 UI, 등간격 카테고리 라벨에 유용합니다.

이들 각각은 sizeThatFits + placeSubviews + (선택적으로) 커스텀 캐시로 구현할 수 있습니다. 프레임워크는 부모-제안-자식-처리 협상을 처리하고, 개발자는 배치 수학을 처리합니다.

일반적인 레이아웃 실패

깨진 커스텀 레이아웃을 만드는 세 가지 패턴:

제안을 무시하는 하드코딩된 크기. 항상 CGSize(width: 200, height: 100)을 반환하는 레이아웃은 컨테이너에 적응하지 못합니다. 결과: 레이아웃이 시뮬레이터에서는 괜찮아 보이지만 더 작은 화면, 다른 방향, 또는 크기 조정 가능한 컨테이너 내부에서는 깨집니다.

placeSubviews에서 서브뷰 건너뛰기. 모든 서브뷰는 호출당 정확히 한 번 배치되어야 합니다. 일부 조건에 대해 continue를 가진 for 루프는 그 서브뷰들을 배치되지 않은 채로 남깁니다. 그것들은 렌더링된 출력에서 사라집니다.

커스텀 Layout의 자식 내부에서 GeometryReader 사용. GeometryReader는 항상 받은 전체 공간을 자신의 콘텐츠에 제안하므로, 레이아웃의 자식별 제안과 충돌합니다. 이 조합은 말도 안 되는 크기를 만듭니다. 커스텀 레이아웃은 그 안에 GeometryReader를 두면 안 됩니다. 자식이 자신의 할당된 크기를 알아야 한다면, 레이아웃 프로토콜의 제안 메커니즘이 올바른 채널입니다.

Layout을 언제 손에 들어야 하는가 (그리고 언제는 아닌가)

커스텀 Layout이 올바른 도구라는 세 가지 신호:

  1. HStack/VStack/ZStack/Grid 컴포지션으로 표현할 수 없는 모양. 파이 레이아웃, 메이슨리 그리드, 커스텀 flow 줄 바꿈. 내장 프리미티브는 이런 모양으로 컴포즈할 수 없습니다.
  2. 자식별 정보가 위치를 결정. 자식이 부모가 그것들을 배치하는 데 사용하는 우선순위, 가중치, 카테고리를 가진 레이아웃. LayoutValueKey가 올바른 채널입니다.
  3. 레이아웃의 크기 결정이 자식과의 협상에 의존. “가장 긴 줄에 맞는 가장 작은 높이는 얼마인가?” 또는 “N개의 자식에게 동일한 컬럼을 주는 너비는 얼마인가?”를 묻는 레이아웃은 subviews.sizeThatFits(...) 질의에 대한 접근이 필요합니다.

내장 컴포지션으로 충분하다는 세 가지 신호:

  1. 표준 수평/수직/깊이 스태킹. HStack, VStack, ZStack이 일반적인 경우를 다룹니다.
  2. 규칙적인 행/열을 가진 그리드. GridLazyVGrid/LazyHGrid가 대부분의 그리드 경우를 처리합니다.
  3. 약간의 오버레이 위치 지정. .overlay, .background, 정렬을 가진 ZStack이 대부분의 “Y 위에 X” 패턴을 다룹니다.

엄지 손가락 법칙: 내장이 처리하는 모양을 위해 커스텀 Layout을 만들지 마세요. 모양이 진정으로 내장의 표현 집합을 넘어설 때 만드세요.

이 패턴이 iOS 26+ 앱에 의미하는 것

세 가지 시사점.

  1. sizeThatFits에서 제안을 존중하라. proposal에 관계없이 동일한 크기를 반환하는 레이아웃은 SwiftUI의 레이아웃 시스템에 적절하게 참여하지 않습니다. 제안을 읽고, 그에 적합한 크기를 반환하세요.

  2. 구조화된 부모-자식 통신에 LayoutValueKey를 사용하라. view-modifier로 첨부된 키를 통해 데이터를 전달하는 것은 SwiftUI 네이티브 패턴입니다. 레이아웃 수준 결정에 특화된 데이터를 위해 @Environment나 커스텀 PreferenceKey에 손을 뻗지 마세요. LayoutValueKey가 그것을 위한 타입화된 채널입니다.

  3. 측정이 비쌀 때만 캐시를 만들어라. 기본 Void 캐시는 대부분의 레이아웃에 괜찮습니다. 동일한 비싼 계산이 sizeThatFitsplaceSubviews 모두에 나타날 때만 커스텀 캐시 타입에 손을 뻗으세요.

전체 Apple Ecosystem 클러스터: 타입화된 App Intents, MCP 서버, 라우팅 질문, Foundation Models, 런타임 vs 도구 LLM 구분, 세 가지 표면, 단일 진실 소스 패턴, 두 개의 MCP 서버, Apple 개발을 위한 훅, Live Activities, watchOS 런타임, SwiftUI 내부, RealityKit의 공간 멘탈 모델, SwiftData 스키마 규율, Liquid Glass 패턴, 멀티 플랫폼 출시, 플랫폼 매트릭스, Vision 프레임워크, Symbol Effects, Core ML 추론, Writing Tools API, Swift Testing, Privacy Manifest, 플랫폼으로서의 Accessibility, SF Pro 타이포그래피, visionOS 공간 패턴, Speech 프레임워크, SwiftData 마이그레이션, tvOS 포커스 엔진, @Observable 내부, 내가 글쓰기를 거부하는 것. 허브는 Apple Ecosystem Series에 있습니다. 더 넓은 iOS-with-AI-agents 컨텍스트는 iOS Agent Development guide를 참조하세요.

FAQ

그냥 GeometryReader를 사용하면 안 되나요?

GeometryReader는 항상 받은 전체 크기를 자신의 콘텐츠에 제안합니다(콘텐츠가 무엇을 원하는지에 대한 의견이 없습니다). 결과적으로 GeometryReader 내부의 어떤 뷰든 리더가 제약하지 않는 축에 대해 infinity가 제안되며, Text 같은 뷰는 욕심스럽게 자체 크기를 결정합니다. 컴포지션이 자신과 싸웁니다. 리더는 변경 없이 통과시키고, 콘텐츠는 최대 크기를 요청하며, 레이아웃은 깨집니다. Layout이 올바른 도구인 이유는 개발자가 제안된 크기에 대해 명시적인 자식별 결정을 내릴 수 있게 하기 때문입니다.

커스텀 HStack 대체품을 작성할 수 있나요?

네. HStack 동등 커스텀 Layout은 자식들의 선호 크기를 읽고, 너비를 합산하고, 최대 높이를 취하고, 왼쪽에서 오른쪽으로 배치합니다. 실제 HStack은 더 많은 일을 합니다(간격, 정렬, 레이아웃 우선순위 해결). 하지만 기본 모양은 Layout에서 간단합니다. 이 연습은 프로토콜이 어떻게 작동하는지 내면화하는 유용한 방법입니다.

커스텀 레이아웃에서 .layoutPriority(_:)를 어떻게 지원하나요?

LayoutSubview 프록시의 전용 priority: Double 프로퍼티를 통해 읽으세요: subview.priority. SwiftUI는 .layoutPriority(_:)를 공개 LayoutValueKey가 아닌 프록시에 직접 노출합니다. 기본값은 0입니다. 추가 공간을 분배할 때(우선적으로 높은 우선순위 자식에게 주기) 또는 잘라낼 때(낮은 우선순위 자식을 먼저 잘라내기) 우선순위를 사용하세요.

proposal: .infinityproposal: .zero의 차이는 무엇인가요?

.infinity는 각 축에서 최대 크기를 제안합니다(width: .infinity, height: .infinity). 욕심스러운 제안에 응답하는 자식들(예: Color)은 사용 가능한 전체 공간을 차지합니다. .zero는 최소 크기를 제안합니다(width: 0, height: 0). 자식들은 최소 크기를 반환합니다(Text는 가장 긴 끊을 수 없는 토큰의 크기를 반환합니다). 둘은 자식의 크기 결정 범위를 측정하는 데 유용한 끝점입니다. 많은 레이아웃은 “이상적인 크기는 무엇인가?”를 묻기 위해 .unspecified(둘 다 nil)를 사용합니다.

Layout이 watchOS, tvOS, visionOS에서 작동하나요?

네. Layout 프로토콜은 SwiftUI의 크로스 플랫폼 코어에 있습니다. 커스텀 레이아웃은 iOS, iPadOS, macOS, watchOS, tvOS, visionOS에서 동일한 방식으로 작동합니다. 클러스터의 Apple Platform Matrix 포스트는 플랫폼 포함이 제품 결정이라고 주장합니다. SwiftUI Layout 메커니즘은 여러 플랫폼이 적용되는 경우에 대해 플랫폼 무관입니다.

Layout@Observable 모델과 어떻게 상호 작용하나요?

Layout은 옵저버블 상태를 직접 보유하지 않는 구조체입니다. 변화를 추적하지 않습니다. 모델이 업데이트되면 부모 뷰의 body가 재평가되고, 이는 body가 생성한 자식들과 함께 Layout이 다시 실행되게 합니다. Layout은 자체 옵저베이션 훅이 아니라, 자신이 살고 있는 body를 통해 반응적입니다. 클러스터의 @Observable internals 포스트가 옵저베이션 측면을 다룹니다.

참고문헌


  1. Apple Developer Documentation: Layout. sizeThatFitsplaceSubviews 요구사항을 다루는 프로토콜 레퍼런스이며, 선택적인 makeCache, updateCache, spacing, 명시적 정렬 훅을 포함합니다. 

  2. Apple Developer Documentation: sizeThatFits(proposal:subviews:cache:)placeSubviews(in:proposal:subviews:cache:). Layout 프로토콜의 두 가지 필수 메서드. 

  3. Apple Developer Documentation: ProposedViewSize. 부모의 크기 제안을 담는 두 옵셔널 CGFloat 타입이며, 관례 값 .unspecified, .zero, .infinity를 가집니다. 

  4. Apple Developer Documentation: LayoutSubview. Layout 메서드 내부에서 자식 뷰를 나타내는 프록시 타입이며, 선호 크기를 질의하기 위한 sizeThatFits(_:)와 위치 지정을 위한 place(at:anchor:proposal:)를 가집니다. 

  5. Apple Developer Documentation: LayoutValueKeylayoutValue(key:value:). 자식에서 부모로의 레이아웃 수준 데이터를 위한 타입화된 채널이며, LayoutSubview의 서브스크립트를 통해 접근됩니다. 

  6. Apple Developer: Composing custom layouts with SwiftUI. 캐싱, 정렬 가이드, 그리고 내장 컨테이너 대신 Layout에 손을 뻗는 시기를 다루는 Apple 가이드. 

관련 게시물

HealthKit + SwiftUI on iOS 26: Authorization, Sample Types, and Cross-Platform Patterns

Real production patterns from Water (water tracking, HKQuantitySample) and Return (mindful sessions, HKCategorySample). …

17 분 소요

Liquid Glass in SwiftUI: Three Patterns From Shipping Return on iOS 26

Apple's Liquid Glass is a one-line SwiftUI API. Three patterns from Return go beyond .glassEffect(): glass on text via C…

19 분 소요

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 분 소요