00:00:00리액트 서버 컴포넌트. 좋거나 싫거나. 요새는 대부분 싫어하는 것 같지만,
00:00:04이제 상황이 바뀔지도 모르겠습니다. TanStack이 참전했거든요. 맞습니다, 이제 TanStack
00:00:08서버 컴포넌트가 생겼고, Next.js와는 꽤 다른 접근 방식을 취했습니다. 한번 살펴보죠.
00:00:13우선 많은 사람들을 안심시켜 줄 만한 그들의 발표 게시물 중 한 단락으로 시작하겠습니다.
00:00:21이렇게 적혀 있네요. "대부분의 사람들은 이제 리액트 서버 컴포넌트를 서버 우선
00:00:26방식으로 생각합니다. 서버가 트리를 소유하고, useClient가 상호작용이 필요한 부분을 표시하며, 프레임워크 규칙이
00:00:31전체 구조를 결정하죠. 이는 리액트 서버 컴포넌트를 유용한
00:00:35프리미티브에서 앱 전체가 중심을 잡아야 하는 존재로 바꿔버립니다. 우리는 여러분이
00:00:40리액트 서버 컴포넌트의 가치를 얻기 위해 그 모델 전체를 처음부터 강제로 받아들일 필요는 없다고 생각합니다."
00:00:45본질적으로 그들이 말하는 건 Next.js처럼
00:00:48기본적으로 서버 컴포넌트로 두고 상호작용이 필요할 때마다 useClient 지시어를 붙여야 하는
00:00:52방식을 따르고 싶지 않다는 겁니다. 대신 TanStack은 리액트 서버 컴포넌트를
00:00:57클라이언트에서 JSON을 가져오는 것만큼 세밀하게 사용할 수 있다면 어떨까라는 관점에서 접근합니다. 그런 목표를 염두에 두고,
00:01:01그들이 실제로 어떻게 서버 컴포넌트를 구현했는지 살펴보겠습니다. 스포일러를 하자면 저는 그 방식이 정말 마음에 듭니다.
00:01:06지금 제가 가진 건 일반적인 TanStack Start 애플리케이션이라서 현재는 모든 것이
00:01:10클라이언트 컴포넌트가 될 겁니다. 제가 한 거라곤 서버 컴포넌트를 실행하기 위해 필요한
00:01:15몇 가지 설치 단계를 거친 게 전부인데, 본질적으로는 패키지를 몇 개 설치하고
00:01:18vconfig를 수정하는 작업입니다. 현재 페이지 모습은 이렇습니다. 여기에 Greeting 컴포넌트가 있는데
00:01:22현재는 클라이언트 컴포넌트이고, 코드상으로는 말 그대로 단순한 리액트 컴포넌트 하나입니다.
00:01:27그리고 아래쪽에는 일반적인 TanStack 라우트가 있고 여기서 Greeting 컴포넌트를 사용하고 있습니다. 자, 이제
00:01:32Greeting 컴포넌트에서 서버 측 로직을 수행하고 싶다고 가정해 봅시다. 저는
00:01:36운영체제의 호스트네임을 가져오고, 서버에서만 사용할 수 있는 환경 변수도
00:01:40가져와서 이게 실제로 서버에서 실행되고 있음을 보여주려고 합니다. 현재 상태에서 os.hostname을 사용하려고 하면
00:01:45이건 노드 함수라서 브라우저에서는 사용할 수 없기 때문에 작동하지 않을 겁니다.
00:01:49그럼 어떻게 해야 할까요? Greeting 컴포넌트를 가져와서 서버에서 렌더링해야 하는데,
00:01:53TanStack Start에서 그렇게 하는 첫 번째 단계는 간단한 서버 함수를 사용하는 것입니다. 보시다시피
00:01:58여기 getGreeting이라는 함수가 하나 있고, 그 안에서 하는 일이라곤 새로운 renderServer
00:02:01Component 함수를 사용해서 컴포넌트를 그 안에 넣고, 반환된 렌더링 가능한 서버 컴포넌트를
00:02:06돌려주는 것뿐입니다. 이건 그냥 컴포넌트에 대한 GET 요청을 만드는 것만큼이나 간단하게 생각하시면 됩니다.
00:02:10그다음 할 일은 우리가 만든 서버 함수로부터 컴포넌트를 단순히 페치해 오는 것입니다.
00:02:14라우트의 로더 안에서 할 수 있는데, getGreeting을 await로 기다린 다음
00:02:18그 결과값을 반환하면 여전히 렌더링 가능한 서버 컴포넌트입니다. 그리고 이것을
00:02:23라우트 안에서 useLoaderData를 사용해 가져와 아래처럼 컴포넌트를 사용하면 됩니다. 이렇게 해서
00:02:27첫 번째 TanStack 서버 컴포넌트가 완성되었습니다. 보시다시피 os.hostname이 이제 잘 작동하고,
00:02:32서버에서만 접근 가능한 환경 변수들도 가져오고 있습니다. 제가 이 구현 방식에서
00:02:36정말 마음에 드는 점은, 유일하게 새로 추가된 것이 render
00:02:41ServerComponent 함수라는 점입니다. 나머지는 그냥 일반적인 TanStack Start 코드일 뿐이죠. 이것을
00:02:46단순히 JSON 데이터를 반환하는 방식으로 바꿔도 똑같은 방식으로 페치할 수 있습니다. 또한 저는
00:02:51이 구현 방식이 코드가 실제로 어디서 실행되는지 매우 명확하게 보여준다고 생각합니다. 모든 작업을
00:02:55서버 함수 안에서 수행하기 때문에 서버에서 실행될 것이라는 점이 꽤 분명하고,
00:02:59renderServerComponent 안에서도 마찬가지입니다. 사실 제 데모 코드를 개선할 수 있을 것 같은데,
00:03:03위에서 서버에서 실행되길 바랐던 함수들을 가져와서
00:03:07서버 함수 내부에 넣고, 그 값들을 Greeting 컴포넌트에 props로 전달하면
00:03:12이제 Greeting 컴포넌트는 클라이언트나 서버 어디서든 사용할 수 있는
00:03:16순수한 컴포넌트가 됩니다. 저는 서버에서 실행하고 싶은 모든 로직을
00:03:21여기 서버 함수 내부에서 실행하고 있습니다. 다시 말하지만, 서버에서 실행될 것이라는 점이 매우 명확합니다. 이건
00:03:25말 그대로 Next.js에서 사용된 로직과 정확히 반대되는 느낌인데, 정말 마음에 듭니다.
00:03:30리액트를 평소처럼 생각할 수 있다는 뜻이죠. 전부 클라이언트 우선으로 시작해서, 원할 때
00:03:34서버 컴포넌트를 그 위에 추가하면 됩니다. 하지만 서버 컴포넌트 안에 클라이언트 컴포넌트를 사용하고 싶다면 어떨까요?
00:03:38트리 안에 중첩해서 말이죠. 예를 들어 여기에 클릭할 때마다 카운터가 올라가는
00:03:43카운터 버튼을 추가하고 싶다고 해봅시다. 제가 단순히 서버 컴포넌트에 onClick과
00:03:47useState 호출을 추가하려고 하면, 완전히 망가집니다. useState는
00:03:52함수가 아니거나 그 반환값이 반복 가능하지 않다고 나오는데, 그건 Greeting이 서버
00:03:56컴포넌트로 사용되고 있기 때문입니다. 이런 클라이언트 기능을 사용할 수 없는 거죠. 이걸 해결하려면 두 가지 방법이 있는데, 두 번째
00:04:01방법이 확실히 더 좋지만 첫 번째 방법은 Next.js를 사용해 본 사람들에게 익숙하게 느껴질 겁니다.
00:04:05단순히 그 로직을 별도의 컴포넌트로 옮기고 useClient
00:04:10지시어를 사용하면 됩니다. 이건 TanStack Start 서버 컴포넌트에서도 작동합니다. 그냥
00:04:14서버 컴포넌트 안에서 그 컴포넌트를 사용하면 이제 아주 잘 작동합니다. 하지만 이 접근 방식의 단점은
00:04:18클라이언트 컴포넌트의 렌더링을 제어하는 서버 컴포넌트가 생겨버린다는 점이며,
00:04:22이게 조금 지저분해지기 시작할 수 있다는 겁니다. 예를 들어 카운터 컴포넌트가 트리 어디에 있는지
00:04:28찾으려면 라우트로 가서 Greeting 서버
00:04:32컴포넌트가 있다는 걸 보고, 다시 Greeting 서버 컴포넌트로 올라가서 그 안에서
00:04:37클라이언트 컴포넌트가 있는 걸 확인해야 하죠. 이런 복잡함이 계속 쌓이면서 서버와 클라이언트의
00:04:42경계가 조금 불분명해질 수 있습니다. 하지만 TanStack은 그렇게 타협하지 않았죠. 그들은
00:04:47서버가 UI의 모든 클라이언트 형태 부분을 결정할 필요가 없으면 어떨까 하고 자문했고,
00:04:51이것이 완전히 새로운 개념인 '복합 컴포넌트(Composite Components)'를 만드는 계기가 되었습니다. 하나를 사용하려면, 우선
00:04:56서버 컴포넌트에서 클라이언트 컴포넌트를 제거하고
00:05:00Greeting 컴포넌트의 모든 자식 컴포넌트로 대체하겠습니다. 다음으로, 서버 함수에서
00:05:05반환하는 값도 변경해야 합니다. 렌더링 가능한 서버 컴포넌트를 반환하는 대신, 우리는
00:05:09이른바 '복합 소스(Composite Source)'를 반환해야 합니다. 이를 위해 TanStack 서버 컴포넌트 헬퍼
00:05:14함수 중 두 번째인 createCompositeComponent를 사용할 수 있습니다. 여기서 우리는 본질적으로 컴포넌트를 구축하는 건데,
00:05:18여기서 props는 슬롯으로 간주됩니다. 저는 아주 간단한 children 슬롯을 사용하고 있으므로, 복합 컴포넌트의
00:05:22자식으로 넣는 모든 것이 props.children으로 전달되어 우리가 방금 가졌던 Greeting 컴포넌트로
00:05:27통과될 겁니다. 이렇게 하고 나서 다시 해야 할 일은
00:05:31서버 함수에서 복합 컴포넌트를 페치해 오는 것이며, 로더에서 똑같은 방식으로 수행합니다.
00:05:36보시다시피 source를 greeting으로 이름을 바꾸고 useLoaderData로 로드합니다.
00:05:41하지만 유일한 차이점은 이걸 컴포넌트로 직접 사용할 수는 없다는 겁니다. 보시다시피 에러가 발생하죠.
00:05:45복합 컴포넌트를 실제로 렌더링하려면 TanStack 서버 컴포넌트에서 제공하는 CompositeComponent 헬퍼
00:05:49컴포넌트를 사용해야 하며, source prop으로 우리가
00:05:53이전에 생성한 서버 함수에서 페치해 온 복합 컴포넌트를 전달합니다.
00:05:57이렇게 하면 서버 컴포넌트가 이전처럼 렌더링되고, 또한
00:06:01카운터를 복합 컴포넌트의 자식으로 전달하고 있으며, 이게 우리가 설정해 둔 Greeting 컴포넌트로
00:06:05전달되어 props.children으로 들어가므로 모든 것이 이제 잘 작동합니다.
00:06:10또한 카운터 컴포넌트에서 우리가 가졌던 지시어를 제거할 수도 있습니다. 왜냐하면
00:06:14복합 컴포넌트 내부에 있어서 클라이언트 컴포넌트라는 것을 이미 알고 있기 때문에 더 이상 필요가 없거든요.
00:06:18이게 슬롯으로 사용되는 중입니다. 결과만 보면 이전과 똑같은 것 같지만
00:06:23useClient 지시어를 사용하는 것보다 작업이 더 들어간 것 같겠지만, 진정한 강점은 개발자 경험에서 나옵니다.
00:06:27그리고 이건 useClient 모델과는 조금 다른 방식이죠. 우리의
00:06:32서버가 클라이언트 컴포넌트가 어디에 렌더링될지 결정하게 하는 대신, (카운터 컴포넌트를
00:06:36서버 컴포넌트 자체에 넣었을 때처럼 말이죠) 대신 복합 컴포넌트로는 이렇게 말하는 겁니다. 여기
00:06:40슬롯이 있을 거고 클라이언트 컴포넌트를 렌더링할 건데,
00:06:44서버 컴포넌트 자체는 그게 무엇인지 전혀 알 필요가 없다는 거죠. 우리는 나중에 클라이언트 코드에서 그 부분을 추가합니다.
00:06:48그러니 모든 클라이언트 기반 컴포넌트를 클라이언트 코드 자체 내에서 처리하는 거죠. 이건 시작에 불과합니다.
00:06:53이 게시물 페이지처럼 더 복잡한 페이지를 보면, 이 게시물은 서버에서 렌더링되고 있는데
00:06:58제가 해결하고 싶은 두 가지 문제가 있습니다. 첫 번째는 게시물 좋아요나
00:07:03작성자 팔로우 같은 동작을 추가하고 싶은데, 여기 제목 위에 넣고 싶고
00:07:08클라이언트 컴포넌트를 사용하고 싶다는 겁니다. 현재는 단순히 children 슬롯 패턴을 사용하고 있는데, 만약 제가
00:07:12포스트 액션을 아래쪽에 추가하면 댓글이 있는 곳으로 들어가 버릴 겁니다. 컴포넌트가 그렇게
00:07:17설정되어 있기 때문이죠. 그래서 서버 컴포넌트에게 특정 클라이언트
00:07:22컴포넌트를 어디에 배치할지 알려줄 방법이 필요합니다. 그다음 두 번째 문제는, 팔로우 작성자 버튼이 있다고 치면
00:07:27이 게시물 페이지는 현재 게시물 작성자가 누구인지 전혀 모릅니다. 우리는 사실 모든 로직을
00:07:32서버 컴포넌트 자체로 오프로드해 버렸거든요. 만약 아래쪽 클라이언트 컴포넌트에서 작성자를 가져오고 싶다면
00:07:37포스트에 대한 JSON을 페치해서 작성자를 가져와야 하는데, 그건 정말 좋은
00:07:42패턴이 아니죠. 데이터를 두 번 가져오게 되는 셈이니까요. 다행히 TanStack에는 복합 컴포넌트에서 사용할 수 있는
00:07:46children 외에도 두 가지 다른 슬롯 타입이 있습니다. 첫 번째는
00:07:50렌더 프롭(render props)입니다. 이건 본질적으로 리액트 요소를 반환하는 함수인 모든 prop을 의미하므로
00:07:56이름은 꼭 renderActions가 아니어도 됩니다. 여기서는 제가 서버 컴포넌트가 전달해주길 바라는 데이터가
00:07:59무엇인지 말하고 있는데, 바로 post ID와 author ID입니다.
00:08:04이제 복합 컴포넌트에서 해야 할 일은 우리가 전달하는 이 함수를
00:08:08결국 렌더링될 컴포넌트가 위치할 곳에 그냥 사용하기만 하면 됩니다.
00:08:12제 경우에는 카드 헤더 아래에 위치하고 싶으니 props.render
00:08:16actions로 호출하면 됩니다. 옵셔널을 사용할 수 있으니 전달되지 않아도 고장 나지 않고
00:08:20렌더링만 되지 않을 뿐입니다. 그리고 우리가 원하는 정보를 서버 컴포넌트에서
00:08:24클라이언트 컴포넌트로 전달할 수도 있습니다. 그 후 우리의 복합 컴포넌트는 방금 만든 renderActions
00:08:28prop을 허용할 것이고, 값으로는 단순히 포스트 ID와 작성자 ID를
00:08:32인자로 받는 함수를 전달하면 서버가 그 인자를 채워줄 것입니다. 그러고 나면 포스트 액션
00:08:36클라이언트 컴포넌트를 렌더링하고 그 데이터를 props로 전달하기만 하면 됩니다. 이제
00:08:41여기에 좋아요를 누르고 게시물 링크를 복사하거나, '팔로우 작성자' 버튼을 클릭할 수 있는 버튼이 생겼는데
00:08:45이 페이지에서는 전혀 가져온 적이 없음에도 작성자 이름을 알고 있습니다.
00:08:49서버 컴포넌트에서만 가져오고, 그 서버 컴포넌트가 그 데이터를
00:08:53클라이언트 컴포넌트로 전달해주고 있는 겁니다. 이제 우리가 이전에 말했던, 서버 컴포넌트가
00:08:57클라이언트 컴포넌트 렌더링을 담당해서는 안 된다는 논리가 깨진다고 생각할 수도 있지만, 그렇지 않습니다.
00:09:01그 이유는 슬롯은 사실 불투명(opaque)하기 때문입니다. 서버 컴포넌트는
00:09:06그 안에 뭐가 있는지 전혀 모르고, 그저 무언가가 여기에 들어가고 이 값들을 전달받아야 한다는 것만 압니다.
00:09:10이 경우에는 post ID와 author ID죠. 이 함수는 서버에서 실행되지 않습니다. 대신
00:09:15서버는 단순히 데이터를 전달해야 한다는 것을 알고 있고, 클라이언트 쪽에서
00:09:19비로소 함수가 실제로 실행되고 컴포넌트가 렌더링되는 겁니다. 정확히 같은 내용이
00:09:23세 번째 슬롯 타입인 컴포넌트 프롭(component props)에도 적용됩니다. 이건 렌더 프롭보다
00:09:28사실 더 간단합니다. 클라이언트 컴포넌트를 반환하는 함수를 가지는 대신
00:09:33우리는 그냥 클라이언트 컴포넌트 자체를 prop으로 전달하기만 하면 됩니다. 그다음 우리의 복합 컴포넌트
00:09:38정의 부분에서 post ID와 author ID를 props로 가지는 리액트 컴포넌트를
00:09:42prop으로 허용하겠다고 명시하면 컴포넌트 자체 내에서 이걸 사용할 수 있습니다. 컴포넌트 프롭을
00:09:47일종의 플레이스홀더처럼 생각할 수 있습니다. 서버 컴포넌트는 그곳에
00:09:51데이터가 필요한 어떤 컴포넌트가 들어올 것임을 알고 있지만(우리의 경우 post ID와 author ID), 그
00:09:56컴포넌트가 정확히 무엇인지는 상관하지 않습니다. 해당 props를 받기만 하면 되니까요. 그래서 제 포스트 액션 컴포넌트를
00:10:01fakePostActions라는 다른 것으로 변경하고 저장하면, 이것이
00:10:05여전히 렌더링되는 것을 볼 수 있습니다. 이 컴포넌트를 렌더링할 책임은 클라이언트에게 있기 때문이며,
00:10:10서버는 데이터만 제공할 뿐입니다. 문서를 살펴보면, 어느 접근 방식을 택하든
00:10:14컴포넌트 프롭을 쓰든 렌더 프롭을 쓰든 별다른 차이는 없어 보입니다.
00:10:18취향 차이일 수 있습니다. 제가 볼 수 있는 유일한 차이는 서버에서 받은
00:10:22데이터를 수정하고 싶을 때 정도인데, 이 경우에는 함수일 뿐이므로
00:10:26post ID와 author ID를 원하는 대로 처리한 다음 컴포넌트로 전달할 수 있습니다.
00:10:31반면에 컴포넌트 프롭을 사용하면 컴포넌트 자체를 전달하게 되고 서버가
00:10:36props 전달을 처리하게 되죠. TanStack 서버 컴포넌트의 기본은 여기까지입니다. 하지만
00:10:40더 좋은 점들이 많습니다. 예를 들어 페이지 대부분이 서버에서 렌더링되길 원해서
00:10:44헤더, 콘텐츠, 푸터 컴포넌트가 있고 이들을 모두 서버에서
00:10:49렌더링하고 싶다면, 모든 걸 하나의 renderServerComponent 함수에 묶을 필요가 없습니다. 실제로
00:10:53Promise.all을 사용해서 세 개의 다른 함수로 나누고, 단순히 단일 서버 함수에서
00:10:58객체로 반환할 수 있습니다. 하지만 그중 하나가 로딩이 오래 걸리면 어떻게 될까요? 그렇다면
00:11:03전체 서버 함수가, 결과적으로 전체 페이지가 느려질 겁니다. 하지만 그것도 걱정 마세요.
00:11:07실제로 우리가 할 수 있는 건 renderServerComponent 함수를 await로 기다리는 대신
00:11:12그것이 생성하는 promise를 반환하고, 클라이언트에서 use 훅과
00:11:16서스펜스(Suspense) 경계를 활용해 스켈레톤을 로드하는 것입니다. 그럼 서버 컴포넌트는 준비되는 대로 로드될 겁니다.
00:11:21저는 TanStack이 취한 이런 접근 방식이 정말 마음에 듭니다. 강압적으로 느껴지지 않고
00:11:25억지로 채택할 필요가 없으며, 이상한 우회 방법 없이도 사용할 수 있습니다. 게다가 실제로 사용하게 될 때
00:11:31서버 컴포넌트 자체는 사실 세 개의 새로운 함수일 뿐이고, 나머지는 그냥 평범한
00:11:36TanStack Start 서버 함수들입니다. 이미 제가 사용하고 있던 것이고, 그냥 데이터를 페치하는 것처럼 간단하죠.
00:11:41이건 또한 TanStack Query 같은 도구와도 잘 통합된다는 뜻인데, 제가
00:11:45꼭 그렇게 할 예정입니다. 그리고 캐싱 같은 것들도 더 간단해집니다. 원한다면
00:11:49CDN에서 GET 요청에 대한 응답을 그냥 캐시해 버릴 수도 있겠죠. 저는 확실히 이것들을 더 탐구해 볼 겁니다.
00:11:54그러니 여러분은 이것들에 대해 어떻게 생각하는지 아래 댓글로 알려주시고, 더 많은 영상을 보고 싶으시면
00:11:59구독해 주세요. 그럼 항상 그렇듯 다음 영상에서 뵙겠습니다.