우리 이제 터보팩 시대인가요?

VVercel
컴퓨터/소프트웨어AI/미래기술

Transcript

00:00:00(경쾌한 음악) 네, 여러분 안녕하세요, 감사합니다.
00:00:07저는 루크 샌드버그입니다.
00:00:09저는 Vercel의 소프트웨어 엔지니어이며, Turbo Pack 개발을 담당하고 있습니다.
00:00:12Vercel에 온 지 약 6개월 정도 되었는데요,
00:00:15그 시간 동안 제가 직접 하지 않은 멋진 작업들에 대해 이 무대에서 말씀드릴 기회를 얻었습니다.
00:00:23Vercel에 오기 전에는 Google에 있었습니다.
00:00:26그곳에서 사내 웹 툴체인 작업을 했고,
00:00:29TSX를 자바 바이트코드로 컴파일하는 컴파일러를 만들거나 클로저 컴파일러 작업을 하는 등 특이한 일들을 했습니다..
00:00:37그래서 Vercel에 왔을 때, 마치 다른 행성에 발을 디딘 것 같았어요. 모든 것이 달랐죠.
00:00:45팀에서 하는 일들과 우리의 목표에 꽤 놀랐습니다.
00:00:50오늘 저는 Turbo Pack에서 내린 몇 가지 설계 결정과 그 결정들이 우리가 이미 달성한 환상적인 성능을 계속해서 발전시키는 데 어떻게 기여할지 말씀드리겠습니다.
00:01:01이해를 돕기 위해, 이것이 우리의 전반적인 설계 목표입니다.
00:01:06이것만 봐도 우리가 어려운 선택을 했다는 것을 바로 짐작할 수 있습니다.
00:01:14그럼 콜드 빌드는 어떨까요?
00:01:17그것들도 중요하지만, 우리의 생각은 애초에 콜드 빌드를 경험할 일이 없어야 한다는 것입니다.
00:01:22오늘 강연은 바로 이 점에 초점을 맞출 것입니다.
00:01:24기조연설에서 번들링 성능 향상을 위해 증분성을 어떻게 활용하는지 잠시 들으셨을 겁니다.
00:01:31증분성을 위한 핵심 아이디어는 캐싱입니다.
00:01:35번들러가 하는 모든 작업을 캐시 가능하게 만들고 싶습니다.
00:01:38그래서 변경 사항이 생길 때마다 해당 변경과 관련된 작업만 다시 하면 되도록 말이죠..
00:01:43다른 말로 하자면,
00:01:44빌드 비용은 애플리케이션의 크기나 복잡성보다는 변경 사항의 크기나 복잡성에 비례해야 한다는 것입니다.
00:01:53이렇게 하면 아무리 많은 아이콘 라이브러리를 가져와도 Turbo Pack이 개발자에게 계속해서 좋은 성능을 제공할 수 있습니다.
00:02:01이 아이디어를 이해하고 설명하기 위해, 세상에서 가장 간단한 번들러를 상상해 봅시다.
00:02:07아마 이런 모습일 겁니다..
00:02:09자, 여기 우리의 '아기 번들러'가 있습니다.
00:02:12이것은 슬라이드에 넣기에는 코드가 좀 많을 수도 있지만, 앞으로는 더 복잡해질 겁니다.
00:02:17여기서는 모든 진입점을 파싱합니다.
00:02:20그리고 가져오기를 따라가고,
00:02:21참조를 해결하며,
00:02:23애플리케이션 전체를 재귀적으로 탐색하여 의존하는 모든 것을 찾습니다.
00:02:28마지막으로, 각 진입점이 의존하는 모든 것을 단순히 수집하여 출력 파일에 넣습니다.
00:02:35야호, 아기 번들러가 생겼습니다!
00:02:38물론 이것은 순진한 방식이지만, 증분적인 관점에서 보면, 이 중 어떤 부분도 증분적이지 않습니다.
00:02:45특정 파일은 여러 번 파싱하게 될 것이고, 아마도 몇 번 가져오느냐에 따라 달라지겠죠.
00:02:51이건 정말 좋지 않습니다..
00:02:53React 가져오기는 수백, 수천 번 해결해야 할 것입니다.
00:02:57그러니, 비효율적일 겁니다.
00:03:01이것을 최소한 조금이라도 더 증분적으로 만들려면, 중복 작업을 피할 방법을 찾아야 합니다.
00:03:08캐시를 추가해 봅시다.
00:03:10이것이 우리의 파싱 함수라고 상상해 볼 수 있습니다.
00:03:15아주 간단하죠.
00:03:15아마도 우리 번들러의 핵심 작업일 겁니다.
00:03:19보시다시피, 아주 간단합니다.
00:03:19파일 내용을 읽고, SWC에 넘겨서 AST를 얻습니다.
00:03:25이제 캐시를 추가해 봅시다.
00:03:27좋습니다. 이것은 분명 간단하고 좋은 성과입니다.
00:03:31하지만, 여러분 중 일부는 이전에 캐싱 코드를 작성해 보셨을 겁니다.
00:03:36아마도 여기에 몇 가지 문제가 있을 수 있습니다.
00:03:38예를 들어, 파일이 변경되면 어떻게 될까요?
00:03:41이것은 분명 우리가 신경 써야 할 부분입니다.
00:03:46그리고 파일이 실제 파일이 아니라, 트렌치코트 안에 세 개의 심볼릭 링크처럼 복잡한 구조라면 어떨까요?
00:03:52많은 패키지 관리자들이 의존성을 그런 식으로 구성합니다.
00:03:55우리는 파일 이름을 캐시 키로 사용하고 있습니다.
00:03:59그것으로 충분할까요?
00:04:00예를 들어, 우리는 클라이언트와 서버를 위해 번들링합니다.
00:04:03같은 파일이 둘 다에 포함될 수 있습니다.
00:04:04이게 제대로 작동할까요?
00:04:05우리는 AST를 저장하고 반환하기도 합니다.
00:04:08이제 변형(mutation)에 대해 걱정해야 합니다.
00:04:11그리고 마지막으로, 이것은 파싱하는 데 너무 순진한 방법 아닌가요?
00:04:16모두가 컴파일러에 대한 방대한 설정을 가지고 있다는 것을 압니다.
00:04:21그중 일부는 여기에 포함되어야 합니다.
00:04:23네, 이 모든 것이 훌륭한 피드백입니다.
00:04:27그리고 이것은 매우 순진한 접근 방식입니다.
00:04:32물론, 저는 '네, 이건 작동하지 않을 겁니다'라고 말할 것입니다.
00:04:36그럼 이 문제들을 해결하기 위해 무엇을 해야 할까요?
00:04:39실수 없이 고쳐야 합니다.
00:04:44네, 좋습니다.
00:04:46아마도 이것이 조금 더 나을 겁니다.
00:04:49보시다시피, 여기에는 몇 가지 변환(transform)이 있습니다.
00:04:52각 파일에 대해 맞춤형 작업을 해야 합니다.
00:04:54예를 들어, 다운레벨링을 하거나 'use cache'를 구현하는 것과 같이요..
00:04:58또한 몇 가지 설정도 있습니다.
00:05:00따라서 당연히 캐시 키에 그 설정을 포함해야 합니다.
00:05:04하지만 아마도 여러분은 바로 의심할 겁니다.
00:05:08이게 정확한가요?
00:05:09이름만으로 변환을 식별하는 것이 정말 충분할까요?
00:05:13모르겠습니다. 아마도 그 자체로 복잡한 설정을 가지고 있을 수도 있습니다.
00:05:16그리고, 이 두 개의 JSON 값이 우리가 신경 쓰는 모든 것을 실제로 담아낼 수 있을까요?
00:05:24개발자들이 이것을 유지보수할까요?
00:05:26이 캐시 키들은 얼마나 커질까요?
00:05:29설정 파일의 복사본은 몇 개나 가지게 될까요?
00:05:31저는 실제로 이런 코드를 직접 본 적이 있는데, 이해하기가 거의 불가능하다고 생각합니다.
00:05:37좋습니다. 우리는 무효화(invalidation)와 관련된 다른 문제도 해결하려고 했습니다.
00:05:43그래서 파일 읽기(read file)에 콜백 API를 추가했습니다.
00:05:46이것은 좋습니다.
00:05:47파일이 변경되면 캐시에서 바로 삭제하여 오래된 내용을 계속 제공하지 않도록 할 수 있습니다..
00:05:55하지만 이것은 사실 꽤 순진한 생각입니다.
00:05:57물론 캐시를 삭제해야 하지만,
00:05:58호출자(caller)도 새 복사본을 가져와야 한다는 것을 알아야 하기 때문이죠..
00:06:03좋습니다. 그럼 콜백을 연결하기 시작해 봅시다.
00:06:06좋습니다, 해냈습니다.
00:06:09스택을 통해 콜백을 연결했습니다.
00:06:12여기서 호출자가 변경 사항을 구독할 수 있도록 허용하는 것을 볼 수 있습니다.
00:06:16무엇이든 변경되면 전체 번들을 다시 실행할 수 있고, 파일이 변경되면 호출합니다.
00:06:22좋습니다. 반응형 번들러가 생겼습니다.
00:06:25하지만 이것은 여전히 거의 증분적이지 않습니다.
00:06:28파일이 변경되면 모든 모듈을 다시 탐색하고 모든 출력 파일을 생성해야 합니다.
00:06:37파싱 캐시를 통해 많은 작업을 절약했지만, 이것만으로는 충분하지 않습니다.
00:06:45그리고 마지막으로, 이 모든 다른 중복 작업들이 있습니다.
00:06:49예를 들어, 우리는 가져오기(imports)를 확실히 캐시하고 싶습니다.
00:06:52파일을 여러 번 찾을 수 있고, 그 가져오기가 계속 필요하므로, 그곳에 캐시를 두어야 합니다.
00:06:57그리고,
00:06:58해결 결과(resolve results)는 실제로 꽤 복잡하므로,
00:07:01React를 해결하는 데 했던 작업을 재사용할 수 있도록 확실히 캐시해야 합니다.
00:07:08하지만, 좋습니다. 이제 또 다른 문제가 있습니다.
00:07:11의존성을 업데이트하거나 새 파일을 추가하면 해결 결과가 변경되므로, 그곳에도 또 다른 콜백이 필요합니다.
00:07:18그리고 우리는 출력물을 생성하는 로직도 확실히 캐시하고 싶습니다.
00:07:22HMR 세션을 생각해 보면,
00:07:24애플리케이션의 한 부분만 편집하는데,
00:07:27왜 매번 모든 출력물을 다시 작성해야 할까요??
00:07:31또한, 출력 파일을 삭제할 수도 있으므로, 그곳의 변경 사항도 들어야 할 것입니다.
00:07:39좋습니다.
00:07:39아마도 이 모든 것을 해결했을지 모르지만, 여전히 문제가 있습니다.
00:07:44바로 무엇이든 변경될 때마다 처음부터 다시 시작한다는 것입니다..
00:07:48그래서 이 함수의 전체 제어 흐름은 작동하지 않습니다.
00:07:51단일 파일이 변경되면, 우리는 그 for 루프의 중간으로 바로 건너뛰고 싶을 것이기 때문입니다..
00:07:56그리고 마지막으로, 호출자에게 제공하는 우리의 API도 절망적으로 순진합니다.
00:08:03그들은 아마도 어떤 파일이 변경되었는지 알고 싶어 할 것입니다.
00:08:05그래야 클라이언트에 업데이트를 푸시할 수 있으니까요..
00:08:07네, 맞습니다.
00:08:11그래서 이 접근 방식은 제대로 작동하지 않습니다.
00:08:13그리고 우리가 어떻게든 이 모든 곳에 콜백을 연결했다고 해도,
00:08:17이 코드를 실제로 유지보수할 수 있다고 생각하시나요?
00:08:21여기에 새로운 기능을 추가할 수 있다고 생각하시나요?
00:08:24저는 그렇게 생각하지 않습니다.
00:08:25이것은 그냥 망가질 겁니다.
00:08:28그리고, 그에 대해 저는 '네, 맞습니다'라고 말할 것입니다.
00:08:34그럼 다시 한번, 우리는 무엇을 해야 할까요?
00:08:36LLM과 대화할 때처럼, 먼저 무엇을 원하는지 정확히 알아야 합니다.
00:08:43그리고 그것을 매우 명확하게 표현해야 합니다.
00:08:48그럼 우리는 대체 무엇을 원하는 걸까요?
00:08:50우리는 많은 다른 접근 방식들을 고려했고,
00:08:53팀의 많은 사람들이 실제로 번들러 작업에 대한 많은 경험을 가지고 있었습니다.
00:08:59그래서 우리는 이런 종류의 대략적인 요구 사항들을 도출했습니다.
00:09:02우리는 번들러의 모든 비용이 많이 드는 작업을 확실히 캐시할 수 있기를 원합니다.
00:09:05그리고 이것은 정말 쉬워야 합니다.
00:09:08새로운 캐시를 추가할 때마다 코드 리뷰에서 15개의 댓글을 받아서는 안 됩니다.
00:09:12그리고 저는 개발자들이 올바른 캐시 키를 작성하거나,
00:09:17입력 또는 의존성을 수동으로 추적하는 것을 실제로 신뢰하지 않습니다.
00:09:24그래서 우리가 처리해야 합니다.
00:09:26실수할 여지 없게 만들어야 합니다.
00:09:30다음으로, 변경되는 입력을 처리해야 합니다.
00:09:33이것은 HMR의 큰 아이디어와 같지만, 세션 간에도 마찬가지입니다.
00:09:36주로 파일이겠지만, 설정 값과 같은 것들도 포함될 수 있습니다.
00:09:40그리고 파일 시스템 캐시를 사용하면, 환경 변수와 같은 것들도 결국 포함됩니다.
00:09:45그래서 우리는 반응형이 되기를 원합니다.
00:09:47무엇이든 변경되자마자 다시 계산할 수 있기를 원하고, 모든 곳에 콜백을 연결하고 싶지 않습니다.
00:09:54마지막으로, 우리는 현대적인 아키텍처를 활용하고, 멀티스레딩을 사용하며, 전반적으로 빨라야 합니다.
00:10:02아마도 여러분은 이 요구 사항들을 보면서,
00:10:06일부는 '이게 번들러랑 무슨 상관이지?'라고 생각할 수도 있습니다.
00:10:12그에 대해 저는 물론, 제 경영진이 이 방에 있으니, 그 이야기는 굳이 할 필요가 없다고 말씀드리겠습니다.
00:10:20하지만 사실, 여러분 중 많은 분들이 훨씬 더 명확한 결론에 도달했을 것이라고 짐작합니다.
00:10:24이것은 시그널(signals)과 매우 비슷하게 들립니다.
00:10:28네, 저는 시그널과 같은 시스템을 설명하고 있습니다.
00:10:31이것은 계산을 구성하고, 의존성을 추적하며, 어느 정도의 자동 메모이제이션을 제공하는 방식입니다.
00:10:37그리고 우리는 모든 종류의 시스템에서 영감을 얻었으며,
00:10:40특히 Rust 컴파일러와 Salsa라는 시스템에서 영감을 받았다는 점을 말씀드려야 합니다.
00:10:45관심 있으시다면, Adaptons라고 불리는 이 개념에 대한 학술 문헌도 있습니다.
00:10:51좋습니다.
00:10:51그럼 이것이 실제로 어떻게 작동하는지 살펴보고,
00:10:55그다음에는 JavaScript 코드 샘플에서 Rust로 매우 갑작스럽게 전환할 것입니다..
00:11:01여기 우리가 구축한 인프라의 예시가 있습니다.
00:11:05TurboTask 함수는 우리 컴파일러에서 캐시되는 작업 단위입니다.
00:11:12따라서 이런 식으로 함수에 주석을 달면,
00:11:16우리는 그것을 추적하고,
00:11:18매개변수로부터 캐시 키를 구성할 수 있습니다.
00:11:21이를 통해 필요할 때 캐시하고 다시 실행할 수 있습니다..
00:11:28여기 VC 타입은 시그널(signals)과 같다고 생각할 수 있습니다.
00:11:32이것은 반응형 값이며,
00:11:33VC는 'value cell'의 약자이지만,
00:11:36'signal'이 더 나은 이름일 수도 있습니다..
00:11:39이런 식으로 매개변수를 선언하면,
00:11:41'이것은 변경될 수 있으며,
00:11:43변경되면 다시 실행하고 싶다'고 말하는 것입니다.
00:11:47그럼 어떻게 그것을 알 수 있을까요?
00:11:49우리는 'await'를 통해 이 값들을 읽습니다.
00:11:52이렇게 반응형 값을 'await'하면, 우리는 자동으로 의존성을 추적합니다.
00:11:58그리고 마지막으로, 물론 우리가 원했던 실제 계산을 수행하고, 그것을 셀(cell)에 저장합니다.
00:12:07우리가 의존성을 자동으로 추적했기 때문에, 이 함수가 파일 내용과 설정 값 모두에 의존한다는 것을 압니다.
00:12:17그리고 셀에 새로운 결과를 저장할 때마다,
00:12:21이전 결과와 비교하여 변경되었다면,
00:12:24그 값을 읽은 모든 사람에게 알림을 전파할 수 있습니다.
00:12:29따라서 '변경'이라는 이 개념은 증분성에 대한 우리의 접근 방식의 핵심입니다.
00:12:33네, 다시 말하지만, 가장 간단한 경우는 바로 여기 있습니다.
00:12:37파일이 변경되면, Turbo Pack은 이를 감지하고, 이 함수 실행을 무효화한 다음 즉시 다시 실행합니다.
00:12:45그리고 만약 우리가 우연히 동일한 AST를 생성한다면,
00:12:48우리는 동일한 셀을 계산했으므로 바로 거기서 멈출 것입니다.
00:12:53이제 파일을 파싱하는 경우, AST를 실제로 변경하지 않는 편집은 거의 없습니다.
00:13:00하지만 우리는 Turbo Pack 함수의 근본적인 조합성을 활용하여 이를 더욱 발전시킬 수 있습니다.
00:13:07여기서 우리는 모듈에서 가져오기(imports)를 추출하는 또 다른 Turbo Pack 캐시 함수를 볼 수 있습니다.
00:13:15이것은 번들러에서 매우 흔한 작업이라고 상상할 수 있습니다.
00:13:20애플리케이션의 모든 모듈을 찾기 위해서라도 가져오기를 추출해야 합니다.
00:13:25우리는 그것들을 활용하여 모듈을 청크로 묶는 가장 좋은 방법을 선택합니다.
00:13:29그리고 물론, 가져오기 그래프는 트리 쉐이킹과 같은 기본적인 작업에 중요합니다.
00:13:34따라서 가져오기 데이터를 사용하는 소비자가 너무 많기 때문에 캐시는 매우 합리적입니다.
00:13:41따라서 이 구현은 특별하지 않습니다.
00:13:44이것은 어떤 종류의 번들러에서도 볼 수 있는 것과 같습니다.
00:13:46우리는 AST를 탐색하고,
00:13:48우리가 선호하는 특별한 데이터 구조에 가져오기를 수집한 다음,
00:13:53그것들을 반환합니다.
00:13:55하지만 여기서 핵심 아이디어는 그것들을 다른 셀에 저장한다는 것입니다.
00:13:58따라서 모듈이 변경되면, 우리가 그것을 읽었기 때문에 이 함수를 다시 실행해야 합니다.
00:14:05하지만 모듈에 가하는 변경 사항의 종류를 생각해 보면, 실제로 가져오기에 영향을 미치는 것은 거의 없습니다.
00:14:12모듈을 변경하고, 함수 본문, 문자열 리터럴, 어떤 종류의 구현 세부 사항이든 업데이트합니다.
00:14:20그것은 이 함수를 무효화하고, 우리는 동일한 가져오기 집합을 계산할 것입니다.
00:14:25그리고 이것을 읽은 어떤 것도 무효화하지 않습니다.
00:14:29따라서 HMR 세션에서 이것을 생각해 보면,
00:14:32이것은 파일을 다시 파싱해야 하지만,
00:14:35더 이상 청킹 결정을 어떻게 할지 고민할 필요가 없다는 것을 의미합니다.
00:14:40트리 쉐이킹 결과에 대해 어떤 것도 생각할 필요가 없습니다.
00:14:43그것들이 변경되지 않았다는 것을 알기 때문입니다..
00:14:45따라서 파일을 파싱하고, 이 간단한 분석을 수행한 다음, 바로 출력물을 생성하는 단계로 넘어갈 수 있습니다.
00:14:53그리고 이것이 우리가 정말 빠른 새로고침 시간을 가지는 방법 중 하나입니다.
00:14:57이것은 꽤 명령형입니다.
00:15:02이 기본적인 아이디어를 생각하는 또 다른 방법은 노드 그래프로 보는 것입니다.
00:15:06여기 왼쪽에, 콜드 빌드를 상상할 수 있습니다.
00:15:12처음에는 실제로 모든 파일을 읽고, 모두 파싱하고, 모든 가져오기를 분석해야 합니다.
00:15:17그리고 그 부작용으로, 우리는 애플리케이션의 모든 의존성 정보를 수집했습니다.
00:15:21그리고 무언가 변경되면,
00:15:23우리가 구축한 의존성 그래프를 활용하여 무효화를 전파하고,
00:15:27스택을 거슬러 올라가 Turbo Pack 함수를 다시 실행할 수 있습니다.
00:15:32그래서 만약 새로운 값을 생성하면, 거기서 멈춥니다.
00:15:35그렇지 않으면, 무효화를 계속 전파합니다.
00:15:37아주 좋습니다.
00:15:41아마 짐작하시겠지만, 이것은 우리가 실제로 하는 일에 대한 엄청난 과도한 단순화입니다.
00:15:47오늘날 Turbo Pack에는 약 2,500개의 다른 TurboTask 함수가 있습니다.
00:15:53그리고 일반적인 빌드에서는 말 그대로 수백만 개의 다른 작업을 가질 수 있습니다.
00:15:58그래서 실제로는 아마도 이런 모습에 더 가까울 겁니다.
00:16:01이제 여러분이 이것을 읽을 수 있을 거라고는 기대하지 않습니다.
00:16:04슬라이드에 다 담을 수 없었습니다.
00:16:06그럼 좀 더 넓게 볼까요?
00:16:08좋습니다. 분명히 도움이 되지 않네요.
00:16:14실제로 우리는 Turbo Pack 내부에서 일어나는 일을 추적하고 시각화하는 더 나은 방법들을 가지고 있습니다.
00:16:21하지만 근본적으로, 그것들은 대부분의 의존성 정보를 버리는 방식으로 작동합니다.
00:16:26이제 여러분 중 일부는 시그널 작업 경험이 있을 것이고, 아마도 좋지 않은 경험일 수도 있다고 짐작합니다.
00:16:34저는 개인적으로 스택 트레이스와 디버거에서 함수 안팎으로 이동할 수 있는 것을 좋아합니다.
00:16:41그래서 이것이 완전한 만병통치약이라고 의심할 수도 있습니다.
00:16:45분명히 장단점이 따르죠.
00:16:47네,
00:16:48그래서 그에 대해 저는 물론,
00:16:51'소프트웨어 엔지니어링은 모두 트레이드오프를 관리하는 것'이라고 말씀드리겠습니다.
00:17:01우리는 항상 문제를 정확히 해결하는 것이 아니라,
00:17:04가치를 제공하기 위해 새로운 트레이드오프 세트를 선택하는 것입니다.
00:17:08따라서 Turbo Pack에서 증분 빌드에 대한 우리의 설계 목표를 달성하기 위해,
00:17:14우리는 이 증분 반응형 프로그래밍 모델에 모든 것을 걸었습니다.
00:17:19그리고 이것은 물론 몇 가지 매우 자연스러운 결과들을 가져왔습니다.
00:17:23그래서,
00:17:24아마도 우리는 수동으로 만든 캐싱 시스템과 번거로운 무효화 로직의 문제를 실제로 해결했을지도 모릅니다.
00:17:33그 대가로, 우리는 복잡한 캐싱 인프라를 관리해야 합니다.
00:17:39그리고 물론, 저에게는 그것이 정말 좋은 트레이드오프처럼 들립니다.
00:17:42저는 복잡한 캐싱 인프라를 좋아하지만, 우리 모두는 그 결과와 함께 살아가야 합니다.
00:17:48그래서 첫 번째는 물론 이 시스템의 핵심 오버헤드입니다.
00:17:54주어진 빌드나 HMR 세션에서 생각해 보면, 실제로 많은 것을 변경하지 않습니다.
00:18:04그래서 우리는 애플리케이션의 모든 가져오기와 모든 해결 결과 사이의 모든 의존성 정보를 추적하지만,
00:18:10실제로 변경하는 것은 그중 몇 개에 불과할 것입니다.
00:18:13따라서 우리가 수집하는 대부분의 의존성 정보는 실제로는 전혀 필요하지 않습니다.
00:18:16그래서 이것을 관리하기 위해,
00:18:18우리는 이 캐싱 레이어의 성능을 개선하여 오버헤드를 줄이고,
00:18:23우리 시스템이 점점 더 큰 애플리케이션으로 확장될 수 있도록 하는 데 많은 노력을 기울여야 했습니다.
00:18:30다음으로 가장 분명한 것은 단순히 메모리입니다.
00:18:34캐시는 항상 근본적으로 시간 대 메모리 트레이드오프입니다.
00:18:38그리고 우리의 캐시도 거기서 크게 다르지 않습니다.
00:18:41우리의 간단한 목표는 캐시 크기가 애플리케이션 크기에 비례하여 선형적으로 확장되어야 한다는 것입니다.
00:18:49하지만 다시 말하지만, 오버헤드에 주의해야 합니다.
00:18:51다음 것은 약간 미묘합니다.
00:18:54예상하시겠지만, 번들러에는 많은 알고리즘이 있습니다.
00:18:58그리고 그중 일부는 애플리케이션에 대한 전역적인 이해를 필요로 합니다.
00:19:03음, 그것은 문제입니다. 전역 정보에 의존할 때마다 어떤 변경이든 해당 작업을 무효화할 수 있기 때문입니다.
00:19:10따라서 우리는 이러한 알고리즘을 어떻게 설계하고, 증분성을 유지할 수 있도록 신중하게 구성해야 합니다.
00:19:17그리고 마지막으로, 이것은 아마도 개인적인 불만일 수 있습니다.
00:19:24Turbo Pack에서는 모든 것이 비동기입니다.
00:19:27그래서 이것은 수평적 확장성에는 훌륭하지만,
00:19:29다시 한번,
00:19:30디버깅 성능 프로파일링과 같은 우리의 근본적인 목표에는 해를 끼칩니다.
00:19:38여러분 중 많은 분들이 Chrome 개발자 도구에서 비동기 코드를 디버깅해 본 경험이 있을 것이라고 확신합니다.
00:19:46그리고 이것은 일반적으로 꽤 좋은 경험입니다.
00:19:48항상 이상적인 것은 아니지만요.
00:19:49그리고 LLDB를 사용하는 Rust는 훨씬 뒤떨어져 있다고 장담합니다.
00:19:53그래서 그것을 관리하기 위해, 우리는 맞춤형 시각화, 계측 및 추적 도구에 투자해야 했습니다.
00:20:01그리고 보세요, 번들러가 아닌 또 다른 인프라 프로젝트입니다.
00:20:07좋습니다. 그럼 우리가 올바른 선택을 했는지 한번 살펴봅시다.
00:20:11Vercel에는 매우 큰 프로덕션 애플리케이션이 있습니다.
00:20:17아마 세계에서 가장 큰 것 중 하나일 것이라고 생각하지만, 사실은 정확히 알 수 없습니다.
00:20:21하지만 약 8만 개의 모듈을 포함하고 있습니다.
00:20:23그럼 Turbo Pack이 이 애플리케이션에서 어떻게 작동하는지 살펴봅시다.
00:20:26빠른 새로고침(fast refresh) 면에서는 Webpack이 제공할 수 있는 것을 정말로 압도합니다.
00:20:32하지만 이것은 다소 오래된 소식입니다.
00:20:33개발용 Turbo Pack은 출시된 지 꽤 되었고,
00:20:36저는 모든 분들이 적어도 개발 환경에서는 사용하고 있기를 진심으로 바랍니다.
00:20:39하지만 오늘 여기서 새로운 소식은, 물론 빌드가 안정적이라는 것입니다.
00:20:42그럼 빌드를 살펴봅시다.
00:20:44여기서 이 애플리케이션에 대해 Webpack보다 상당한 우위를 점하는 것을 볼 수 있습니다.
00:20:49이 특정 빌드는 실제로 우리의 새로운 실험적인 파일 시스템 캐싱 레이어를 사용하여 실행되고 있습니다.
00:20:53그래서 94초 중 약 16초는 마지막에 캐시를 비우는 데 사용됩니다.
00:20:59그리고 이것은 파일 시스템 캐싱이 안정화됨에 따라 개선해 나갈 부분입니다.
00:21:04하지만 물론, 콜드 빌드의 특징은 '콜드'하다는 것입니다.
00:21:06증분적인 것이 아무것도 없습니다..
00:21:07그럼 실제 웜 빌드를 살펴봅시다.
00:21:10콜드 빌드에서 얻은 캐시를 사용하면 이것을 볼 수 있습니다.
00:21:14이것은 우리가 오늘날 어디에 와 있는지 살짝 보여주는 것입니다.
00:21:17이런 세분화된 캐싱 시스템 덕분에,
00:21:19실제로 캐시를 디스크에 쓰고,
00:21:21다음 빌드에서 다시 읽어 들여 무엇이 변경되었는지 파악한 다음 빌드를 완료할 수 있습니다.
00:21:26좋습니다.
00:21:27이것은 꽤 좋아 보이지만,
00:21:28많은 분들이 '음,
00:21:29나는 개인적으로 세계에서 가장 큰 Next.js 애플리케이션을 가지고 있지 않은데'라고 생각할 것입니다..
00:21:34그럼 더 작은 예시를 살펴봅시다.
00:21:37react.dev 웹사이트는 훨씬 더 작습니다.
00:21:41또한 React 컴파일러이기 때문에 흥미롭습니다.
00:21:44놀랍지 않게도 React 컴파일러의 초기 채택자입니다.
00:21:47그리고 React 컴파일러는 Babel로 구현되어 있습니다.
00:21:49그리고 이것은 우리의 접근 방식에 일종의 문제입니다.
00:21:51애플리케이션의 모든 파일에 대해 Babel에게 처리를 요청해야 하기 때문입니다..
00:21:55그래서 근본적으로,
00:21:56저는 우리가,
00:21:57아니 제가 React 컴파일러를 더 빠르게 만들 수는 없다고 말씀드리겠습니다.
00:22:01그것은 제 일이 아닙니다.
00:22:02제 일은 Turbo Pack입니다.
00:22:03하지만 우리는 언제 그것을 호출해야 할지 정확히 알아낼 수 있습니다.
00:22:07빠른 새로고침 시간을 보면, 저는 사실 이 결과에 약간 실망했습니다.
00:22:13그리고 밝혀진 바에 따르면, 그 140밀리초 중 약 130밀리초가 React 컴파일러 때문입니다.
00:22:18Turbo Pack과 Webpack 모두 그 작업을 수행합니다.
00:22:22하지만 Turbo Pack을 사용하면,
00:22:24React 컴파일러가 이 변경 사항을 처리한 후,
00:22:26'아,
00:22:26가져오기가 변경되지 않았네'라고 확인할 수 있습니다.
00:22:29출력물에 넣고 계속 진행합니다.
00:22:31다시 한번, 콜드 빌드에서는 이런 일관된 3배의 성능 향상을 볼 수 있습니다.
00:22:37명확히 말씀드리자면, 이것은 제 컴퓨터에서 측정한 것입니다.
00:22:39하지만 다시 말하지만, 콜드 빌드에서는 증분성이 없습니다.
00:22:44그리고 웜 빌드에서는 훨씬 더 좋은 시간을 볼 수 있습니다.
00:22:47다시 말해, 웜 빌드에서는 이미 디스크에 캐시가 있습니다.
00:22:52기본적으로 우리가 해야 할 일은 일단 시작하면,
00:22:54애플리케이션에서 어떤 파일이 변경되었는지 파악하고,
00:22:57해당 작업을 다시 실행한 다음,
00:22:58이전 빌드의 나머지 모든 것을 재사용하는 것입니다.
00:23:01그래서 기본적인 질문은, '우리는 이미 터보인가?'입니다.
00:23:05네.
00:23:06네, 물론 이것은 기조연설에서 논의되었습니다.
00:23:09Turbo Pack은 Next 16부터 안정화되었습니다.
00:23:12그리고 우리는 Next의 기본 번들러이기도 합니다.
00:23:14그래서, 임무 완료입니다. (웃음) 다들 환영합니다.
00:23:17하지만. (웃음) (청중 박수)
00:23:23그리고 기조연설에서 그 '되돌리기'를 보셨다면,
00:23:26그것은 제가 Turbo Pack을 기본으로 만들려고 시도했던 것입니다.
00:23:30세 번 만에 성공했습니다.
00:23:31하지만 제가 다시 한번 여러분께 남기고 싶은 것은 이것입니다.
00:23:35아직 끝나지 않았기 때문입니다.
00:23:37우리는 성능 면에서 아직 할 일이 많고, 파일 시스템 캐싱 레이어 작업을 마무리해야 합니다.
00:23:42개발 환경에서 모두 사용해 보시길 권합니다.
00:23:44이상입니다.
00:23:46정말 감사합니다.
00:23:47저를 찾아와 질문해 주세요.
00:23:49(청중 박수) (경쾌한 음악) (경쾌한 음악)

Key Takeaway

터보팩은 시그널 기반의 증분 반응형 프로그래밍 모델을 통해 번들링 성능을 혁신적으로 개선하여 Next.js의 기본 번들러로 자리매김했으며, 앞으로도 지속적인 최적화를 목표로 합니다.

Highlights

터보팩은 증분 빌드에 중점을 두어 변경 사항의 크기에 비례하는 빌드 비용을 목표로 합니다.

수동 캐싱 및 무효화의 복잡성을 해결하기 위해 시그널 기반의 반응형 프로그래밍 모델을 채택했습니다.

TurboTask 함수와 VC(값 셀)을 통해 자동 의존성 추적 및 캐싱을 구현하여 효율성을 높였습니다.

파일 내용이나 설정 변경 시, AST나 가져오기 같은 중간 결과가 동일하면 상위 작업을 재실행하지 않아 HMR 성능이 크게 향상됩니다.

대규모 프로덕션 애플리케이션에서 웹팩 대비 빠른 새로고침 및 콜드/웜 빌드에서 상당한 성능 우위를 보였습니다.

파일 시스템 캐싱을 통해 세션 간에도 캐시를 재사용하여 웜 빌드 시간을 획기적으로 단축했습니다.

터보팩은 Next.js 16부터 안정화되었으며 Next의 기본 번들러로 채택되었지만, 성능 개선 작업은 계속 진행 중입니다.

Timeline

터보팩 소개 및 증분 빌드의 중요성

발표자는 Vercel의 소프트웨어 엔지니어인 루크 샌드버그로, 터보팩 개발을 담당하고 있습니다. 그는 구글에서의 경험을 바탕으로 터보팩의 설계 목표를 설명합니다. 핵심 목표는 콜드 빌드보다는 증분 빌드에 초점을 맞춰, 번들러의 모든 작업을 캐시 가능하게 만들어 변경 사항의 크기에 비례하여 빌드 비용이 발생하도록 하는 것입니다. 이는 애플리케이션의 크기나 복잡성에 관계없이 개발자에게 일관된 성능을 제공하기 위함입니다.

캐싱과 무효화의 복잡성

발표자는 가장 간단한 번들러("아기 번들러")를 예시로 들어 증분성이 부족한 문제를 지적합니다. 파일 파싱, 가져오기 해결 등 중복 작업이 많아 비효율적입니다. 캐시를 추가하는 과정에서 파일 변경, 심볼릭 링크, 클라이언트/서버 번들링, AST 변형, 복잡한 컴파일러 설정 등 다양한 문제에 직면하게 됩니다. 콜백을 통해 무효화를 처리하려 하지만, 이는 코드를 유지보수하기 어렵게 만들고 여전히 진정한 증분 빌드를 달성하지 못합니다.

터보팩의 설계 원칙: 자동화된 증분성

발표자는 이상적인 번들러가 갖춰야 할 요구사항을 제시합니다. 모든 비용이 많이 드는 작업을 쉽게 캐시할 수 있어야 하며, 개발자가 수동으로 캐시 키를 작성하거나 의존성을 추적할 필요 없이 자동으로 처리되어야 합니다. 또한, 파일 변경이나 설정 값 변경과 같은 입력 변화에 반응적으로 대응하고, 멀티스레딩을 활용하여 전반적으로 빨라야 합니다. 이러한 요구사항은 시그널(signals)과 같은 시스템의 필요성을 암시합니다.

시그널을 활용한 터보팩의 증분 반응형 모델

터보팩은 Rust 컴파일러와 Salsa 시스템에서 영감을 받은 시그널 기반의 증분 반응형 프로그래밍 모델을 채택했습니다. TurboTask 함수는 캐시되는 작업 단위이며, VC(값 셀)은 반응형 값을 나타냅니다. await를 통해 이 값들을 읽으면 자동으로 의존성이 추적되며, 값이 변경될 경우 관련 함수만 다시 실행하고 결과를 비교하여 변경이 없으면 전파를 멈춥니다. 이를 통해 AST 파싱이나 가져오기 추출과 같은 중간 단계의 캐싱을 최적화하여 HMR 세션에서 불필요한 재계산을 방지합니다.

터보팩 아키텍처의 도전 과제와 트레이드오프

터보팩은 약 2,500개의 TurboTask 함수와 수백만 개의 작업을 처리하는 복잡한 노드 그래프로 작동합니다. 이러한 시그널 기반 시스템은 수동 캐싱의 문제를 해결하지만, 복잡한 캐싱 인프라 관리라는 새로운 트레이드오프를 가져옵니다. 주요 도전 과제로는 시스템의 핵심 오버헤드 관리, 메모리 사용량 최적화(애플리케이션 크기에 비례하여 선형적으로 확장), 전역적인 이해를 필요로 하는 알고리즘의 증분성 유지, 그리고 비동기 Rust 코드 디버깅의 어려움이 있습니다. 이를 위해 맞춤형 시각화 및 추적 도구에 투자해야 했습니다.

실제 성능 검증 및 Next.js 통합

발표자는 대규모 프로덕션 애플리케이션(8만 모듈)과 react.dev 웹사이트(React 컴파일러 사용)를 예시로 터보팩의 성능을 시연합니다. 터보팩은 웹팩 대비 빠른 새로고침에서 압도적인 성능을 보였으며, 콜드 빌드와 파일 시스템 캐싱을 활용한 웜 빌드에서도 상당한 속도 향상을 입증했습니다. 특히 웜 빌드에서는 이전 빌드의 캐시를 재사용하여 시작 후 변경된 파일만 다시 처리함으로써 획기적인 시간을 달성했습니다. 터보팩은 Next.js 16부터 안정화되어 기본 번들러로 채택되었으며, 앞으로도 성능과 파일 시스템 캐싱 레이어 개선을 위한 노력을 계속할 것이라고 강조합니다.

Community Posts

View all posts