Next) 서버 컴포넌트(React Server Component)에 대한 고찰

2ast·2023년 5월 29일
188

서론: 트렌디함도 개발자에게 중요한 미덕이다

이번에 회사에서 신규 웹 프로젝트를 진행하기로 결정했는데, 정말 뜬금 없게도 앱 개발자인 내가 이 프로젝트를 리드하게 되었다. 사실 억지로 떠맡게 된 것은 아니고, 새로운 웹 기술 스택을 적용해보고 싶어서 적극 자원한 결과였다.

  • Next12(11) - React Framework
  • Apollo Client - GraphQL Client
  • Emotion - CSS Framework

현재 팀에서 주로 사용하고 있는 기술들이다. 사실 위 나열된 기술들은 메이저하고, 검증되었고, 실제로 유용한 기술들이다. 하지만 최근 몇개월간 프론트엔드 관련 기술 소식을 접하면서 생각보다 프론트엔드쪽의 생태계가 빠르게 변하고 있는 것을 느낄 수 있었고, 이대로 정체되어 있다가는 언젠가 뒤쳐질 수 있겠다는 생각이 들었다.
마침 이번에 시작하는 웹 프로젝트는 사이즈도 작고, 우선순위도 비교적 낮으며, 촉박한 데드라인도 없는 특수한(희귀한) 프로젝트였기에 이런 두려움을 해소하기에는 최적의 기회라고 판단했고, 결국 승낙을 받아낼 수 있었다.

결과적으로 이번 프로젝트에는 이전 버전 대비 굉장히 많은 것이 바뀌었고, 최근 app directory가 stable 배포된 Next13과 apollo client 대비 러닝커브는 높지만 만족도의 고점은 높다고 평가받는 Relay, zero runtime css in js로, css in js의 단점을 보완하고 성능을 개선한 Vanilla Extract를 적용하기로 결정했다.

  • Next13 - React Framework
  • Relay - GraphQL Client
  • Vanilla Extract - CSS Framework

서론이 길었는데, 제목에서 언급했듯이 이번 글에서는 next13의 가장 큰 변경점이자 앞으로 웹 개발의 게임체인저로 거론되고 있는 server component에 대한 고찰을 다뤄보려고한다. 사실 이전에 next를 다뤄본 적도 없고, RSC에 대한 개념을 이번에 처음 접한 입장에서 오직 서치에 기반해 작성하는 내용이기 때문에 사실과 다른 내용이 있다면 부담없이 지적해주길 바란다.

Next13과 Server Component

next가 13버전으로 올라오면서 굉장히 많은 변화가 있었는데, 그 중 가장 파격적인 변화는 바로 app directory의 등장이라고 할 수 있다. 기존 next 프로젝트는 pages라는 폴더를 이용해 스크린간 라우팅을 셋팅할 수 있었는데, next13부터는 app 폴더가 pages 폴더를 대체하게 되었다. 그리고 app directory와 관련해서 우리가 집중할 부분은 app directory내부에서는 모든 컴포넌트가 기본적으로 서버컴포넌트로 동작한다는 사실이다. 만약 app directory 내부에서 클라이언트 컴포넌트를 사용하고 싶다면 파일 최상단에 use client라는 directive를 명시해주면 된다.

"use client";

import {useState} from "react";

const ClientComponent  =() => {
 
 const [state,setState] = useState()

 return <div>Test</div>
}

export default ClientComponent

(그 외 next13에서 달라진 부분이나 app과 pages의 차이는 https://nextjs.org/blog/next-13 여기를 참조)

RSC(React Server Component) vs RCC(React Client Component)

React Server Component(줄여서 RSC)는 React18부터 도입된 개념으로, 말 그대로 서버에서 동작하는 컴포넌트를 가리킨다. ‘서버 컴포넌트가 있으니 그러면 클라이언트 컴포넌트도 있겠네?‘라고 생각한다면 당연히 그렇다. 이전 버전에서 우리가 사용하던 모든 컴포넌트가 바로 클라이언트 컴포넌트다.

방금 언급했듯이 서버 컴포넌트와 클라이언트 컴포넌트의 가장 큰 차이점은 컴포넌트가 렌더링되는 장소가 서버냐 클라이언트냐의 차이다. 서버 컴포넌트는 서버에서 한차례 해석된 이후 클라이언트로 전달되고, 클라이언트 컴포넌트는 클라이언트가 js 번들을 다운로드 받은 후 해석하게 된다.

위 표에서 볼 수 있듯, RSC와 RCC는 렌더링되는 위치가 다르기 때문에 각각 할수있는 역할이 명확하게 구분되어 있다. “즉 RSC가 새로나온 개념이니까 RCC보다 무조건 좋겠지?”라는 접근이 아니라 RSC와 RCC를 적재적소에 배치하여 개발하려는 접근이 필수적이라고 할 수 있다.

RSC의 동작 방식

RSC의 특징을 이해하고 RSC와 RCC를 적재적소에 배치하기 위해서는 실제로 RSC가 어떻게 렌더링되는지 이해할 필요가 있다. 아래와 같이 RSC와 RCC를 적절하게 혼합하여 구성한 스크린이 있다고 가정해 보자.

사용자는 해당 페이지를 띄우기 위해 서버로 요청을 날린다. 그러면 서버는 이때부터 컴포넌트 트리를 root부터 실행하며 직렬화된 json형태로 재구성하기 시작한다.

*직렬화란

여기서 직렬화(serialization)에 대한 이해가 필요하기 때문에 잠깐 짚고 넘어가겠다.

직렬화란 데이터 스토리지 문맥에서 데이터 구조나 오브젝트 상태를 동일하거나 다른 컴퓨터 환경에 저장(이를테면 파일이나 메모리 버퍼에서, 또는 네트워크 연결 링크 간 전송)하고 나중에 재구성할 수 있는 포맷으로 변환하는 과정

쉽게 말해서 특정 개체를 다른 컴퓨터 환경으로 전송하고 재구성할 수 있는 형태로 바꾸는 과정이라고 할 수 있다. 우리가 흔히 사용하는 JSON.stringify함수가 바로 직렬화를 수행하는 함수이며, JSON.parse가 역직렬화를 수행하는 함수다. 주의할점은 모든 객체를 직렬화할 수는 없다는 것이다. 대표적으로 function은 직렬화가 불가능한 객체다. function이 실행코드와 실행 컨텍스트를 모두 포함하는 개념이기 때문인데, 함수는 자신이 선언된 스코프에 대한 참조를 유지하고, 그 시점의 외부 변수에 대한 참조를 기억하고 있다. js의 클로저가 바로 이런 현상을 가리키는 용어이기도 하다.

const a = 100;
    
const sample = ()=>{
    console.log(a)
}

sample() //100

이처럼 함수의 실행 컨텍스트, 스코프, 클로저까지 직렬화할 수는 없기 때문에 function은 직렬화가 불가능한 객체로 분류되는 것이다.

직렬화 과정은 모든 서버 컴포넌트 실행하여 json 객체 형태의 트리로 재구성할 때까지 진행된다. 예를들면 다음과 같다.

<div style={{backgroundColor:'green'}}>hello world</div> //JSX 코드는 createElement의 syntax sugar다.

> React.createElement(div,{style:{backgroundColor:'green'}},"hello world")

> {
  $$typeof: Symbol(react.element),
  type: "div",
  props: { style:{backgroundColor:"green"}, children:"hello world" },
  ...
} //이런 형태로 모든 컴포넌트를 순차적으로 실행한다.

다만 이 과정을 모든 컴포넌트에 대하여 진행하는게 아니라, RCC일 경우 건너뛰게 된다. 하지만 RCC를 서버에서 해석하지 않고 건너 뛴다고해서 비워 둔다면 실제 컴포넌트 트리와 괴리가 생기게 된다. 따라서 RCC의 경우 직접 해석하는 것이 아니라 “이곳은 RCC가 렌더링되는 위치입니다”라는 placeholder를 대신 배치해준다.


{
  $$typeof: Symbol(react.element),
  type: {
    $$typeof: Symbol(react.module.reference),
    name: "default", //export default를 의미
    filename: "./src/ClientComponent.js" //파일 경로
  },
  props: { children: "some children" },
}

아까도 언급했듯이 RCC는 곧 함수이므로, 직렬화를 할 수 없다. 따라서 함수를 직접 참조하는 것이 아니라 “module reference” 라고 하는 새로운 타입을 적용하고, 해당 컴포넌트의 경로를 명시함으로써 직렬화를 우회하고 있다.

이러한 직렬화 작업을 마친 후 생성된 JSON Tree를 도식화하면 다음과 같은 형태를 띠고 있다.

이제 이렇게 도출된 결과물을 Stream 형태로 클라이언트가 전달받게 되고, 함께 다운로드한 js bundle을 참조하여, module reference 타입이 등장할 때마다 RCC를 렌더링해서 빈 공간을 채워놓은 뒤, DOM에 반영하면 실제 화면에 스크린이 보여지게 되는 것이다.

RSC의 제약사항

이 일련의 과정으로부터 우리는 RSC와 RCC를 함께 사용할 때 몇가지 제약사항을 도출해낼 수 있다.

  1. RSC에서 RCC로 function과 같이 직렬화 불가능한 객체를 prop으로 넘겨줄 수 없다.
    - RSC는 서버에서 해석되어 직렬화된 JSON 형태로 변환된다. 그때문에 서버컴포넌트를 설명하는 모든 요소는 ‘직렬화 가능해야 한다’는 전제조건이 붙는다. 만약 RSC가 child에게 function을 prop으로 넘겨주면 위와같이 JSON에 이 사실이 명시되어야 하므로, 에러가 발생할 수 있다.

     {
       $$typeof: Symbol(react.element),
       type: "div",
       props: { 
     		children: {
     		  $$typeof: Symbol(react.element),
               type: {
                 $$typeof: Symbol(react.module.reference),
                 name: "default",
                 filename: "./src/ClientComponent.js"
               },
     		  props: {callback:function}, // 이처럼 JSON에 function이 명시되어야만 한다.
     		  ...
     		}
     	},
       ...
     }

    정정 추가) 하지만 RSC에서 다른 RSC로 function을 넘기는건 아무 문제 없이 가능하다. 정확한 이유는 모르겠지만 어차피 서버에서 렌더링되는 RSC간의 함수 전달을 굳이 client로 넘기는 스트림에 서술할 필요가 없기 때문에 생략하거나 placeholder로 대신한게 아닐까 하고 짐작하고 있다.
    또한, next의 server action을 사용하면 RSC에서 RCC로 함수를 전달할 수도 있는데, 아래와 같이 RSC에서 'use server' directive와 함께 함수를 정의하면 RCC로 넘겨줄 수 있다.

    const ServerComponent = ()=>{
      const add = async (a:number,b:number) =>{
        'use server'
        return a+b
      }
    
      return <div>
            <ClientComponent addFunc={add}/>
        </div>
    }
    

    다만, 해당 function의 params와 return은 모두 직렬화 가능해야한다는 조건이 붙는다. server action으로 선언한 함수를 RSC에서 RCC로 넘겨줄 때는 function 자체를 넘겨준다기보다, api의 명세를 넘겨주고, 함수를 호출하면 서버에 api를 호출하고 그 결과값을 받아오는 것처럼 동작하여 제약사항을 우회하는 것이 아닌가 추측하고 있다. 자세한 내용은 Next 공식 문서의 server action을 살펴보길 바란다.

  2. RCC는 RSC를 직접 return해줄 수 없으며, 반드시 children prop의 형태로 넘겨주어야 한다.

    • 서버에서 모든 RSC가 순차적으로 실행되며, 중간에 RCC를 만나면 placeholder로 표시해두고 넘어간다고 했다. 즉, RCC는 실행되지 않기 때문에 RCC 내부에서 반환되는 RSC또한 (서버 컴포넌트임에도 불구하고) 서버에서 실행되지 못한다. 이러한 경우 해당 RSC는 RCC와 동일하게 클라이언트에서 동작하게 된다.
    • 하지만 children prop을 통해 RSC를 넘기게 되면, 사실상 공통 부모가 렌더링 되는 시점에 RSC가 실행이 되고, 그 결과값을 children으로 전달할 수 있다.
      
      function ParentClientComponent({children}) {
      	...
        return <div onChange={...}>{children}</div>;
      }
      
      function ChildServerComponent() {
      	...
        return <div>server component</div>;
      }
      
      function ContainerServerComponent() {
        return <ParentClientComponent>
      			<ChildServerComponent/>
      	</ParentClientComponent>;
      }
      위와 같이 “ChildServerComponent”는 “ParentClientComponent”의 자식 컴포넌트이지만, 사실상 “ContainerServerComponent”를 공통부모로 갖고있기 때문에 “ContainerServerComponent”가 렌더링되는 시점에 “ChildServerComponent”도 함께 렌더링되어 그 결과값이 “ParentClientComponent”에 넘겨지고 있다.
      {
        // The ClientComponent element placeholder with "module reference"
        $$typeof: Symbol(react.element),
        type: {
          $$typeof: Symbol(react.module.reference),
          name: "default",
          filename: "./src/ParentClientComponent.js"
        },
        props: {
          children: {
            $$typeof: Symbol(react.element),
            type: "div",
            props: {
              children: "server component"
            }
          }
        }
      }
      (JSX와 children prop에 대한 더 자세한 내용은 children prop에 대한 고찰을 봐주세요.(강추👍))

RSC vs SSR(Server Side Rendering)

‘컴포넌트가 서버에서 해석되어 클라이언트로 전달되는게 바로 SSR아니야?’라고 생각될 수 있으나, 실제로 RSC와 SSR은 서버에서 처리한다는 공통점 외에는 각각 해결하고자하는 목표도 다르고, 일어나는 시점과 최종 산출물도 다른 완전히 별개의 개념이다. 따라서 반드시 둘 중 하나를 선택할 필요도 없고 필요에 따라 RSC와 SSR을 함께 사용하면 큰 시너지를 낼 수도 있다.

우리가 작성한 소스코드가 브라우저에 보여지기 위해서는 우선 컴포넌트가 실행되어 데이터가 해석되어야하고, 그 해석된 데이터가 다시 html로 변환하는 과정을 거쳐야한다. RSC는 이중 전자에 해당하는 단계에 관여하고, SSR은 후자에 관여한다.

그런데 여기서 의문이 생길 수 있다. (적어도 나는 의문이었다.)

RSC와 SSR는 공존할 수 있다고 했는데, RSC를 쓰든 RCC를 쓰든 SSR을 적용하면 클라이언트가 받는 최종 산출물은 html로 동일한거 아닌가? 그러면 SSR을 채택했을 때 RSC의 이점은 어디에 있는걸까?

이 의문에 답을 하기 위해서는 next에서 ssr을 어떻게 사용하고 있는지 이해해야한다.

Next.js의 SSR

*CSR vs SSR

이쯤에서 간단히 CSR과 SSR의 개념을 짚고 넘어가겠다.

Client Side Rendering은 말 그대로 클라이언트에서 컴포넌트를 렌더링하는 것을 의미한다. 서버에서 빈 html과 js bundle을 다운로드 받고, 이 js 소스코드를 클라이언트에서 해석해서 처음부터 그려나가게 된다. 때문에 초기 로딩속도가 느리지만, 스크린간 이동이나 인터렉션에 강점이 있다.

반면 Server Side Rendering은 서버에서 컴포넌트를 해석하여 최종 결과물인 html 파일을 내려주는 것을 의미한다. CSR과는 반대로 초기 로딩속도가 빠르지만, 페이지를 이동할때마다 새로운 html을 요청해서 받는 시간이 필요하고, 현재 화면에서도 작은 변경사항이 발생하면 처음부터 html을 다시 로드해야하기 때문에 스크린간 이동이나 인터렉션에 약점이 있다.

사실 next js에서 우리가 사용하는 ssr은 전통적인 의미의 ssr은 아니다. ssr과 csr의 장점만을 취하기 위해 일종의 절충점을 찾은 형태라고 할 수 있다. 즉, 초기 로딩속도가 느리다는 CSR의 단점을 보완하기 위해 초기 로딩시에는 html파일을 SSR을 통해 빠르게 받아오고, 이와 병렬적으로 js번들도 함께 가져와서 미리 받아온 html과 병합하는 hydration과정을 거치는 것이다. 그 결과 빠른 로딩에 강점이 있는 SSR과 인터렉션에 강점이 있는 CSR의 장점을 모두 취할 수 있게 된다.

즉 Next js의 SSR 뿐만 아니라 CSR의 특징도 많이 가지고 있으므로 RSC를 함께 사용했을 때 그 이점이 더욱 크게 극대화될 수 있다.

RSC의 장점

이제 RSC가 어떻게 동작하는지 모두 이해했으니, RSC의 장점을 쉽게 납득할 수 있을 것이다.

Zero Bundle Size

RSC는 서버에서 이미 모두 실행된 후 직렬화된 JSON 형태로 전달되기 때문에 어떠한 bundle도 필요하지 않다. 즉, RSC의 컴포넌트 소스파일 뿐만아니라, RSC에서만 사용하는 외부 라이브러리의 경우에도 번들에 포함될 필요가 없기 때문에 번들사이즈를 획기적으로 감량할 수 있다.

이러한 부분은 Next의 TTI(Time To Interactive) 개선에 크게 기여할 수 있는데, 이전에 살펴봤듯이 Next에서 SSR을 사용한다고 하더라도 초기 로딩속도에 이점이 있을 뿐 CSR과 동일한 사이즈의 js 번들을 다운받아야하기 때문에 TTI는 여전히 CSR 대비 큰 메리트가 없었기 때문이다. 하지만 RSC를 도입하면 다운받아야하는 번들 사이즈가 줄어들게 되므로, TTI에 개선에 기여할 수 있다.

No More getServerSideProps / getStaticProps (app directory)

기존 next에서는 getServerSideProps / getStaticProps라는 함수를 이용해서 서버에 접근했었다. 때문에, Data fetch등을 수행할때는 반드시 getServerSideProps(or getStaticProps)함수를 page 최상단에서 수행하고, 이를 page에 prop으로 넘겨서 사용했어야 했다.

하지만 이 과정은 순수 React와는 괴리가 있어 처음 next를 사용하는 사람들에게 낯설 뿐만 아니라, 무조건 최상단에서 fetch 후 page에 prop으로 넘겨줄 수밖에 없는 구조 때문에, 실제 data를 사용하는 하위 컴포넌트의 depth까지 props drilling이 불가피했다.

반면 RSC는 그 자체가 서버에서 렌더링되므로, 컴포넌트 내부에서 Data Fetch를 실행해도 무방하다. 즉, data가 필요한 컴포넌트에서 직접 data fetch가 가능해졌고, next13의 app directory에서는 기본적으로 모든 컴포넌트가 RSC이기 때문에 더이상 getServerSideProps / getStaticProps는 불필요한 함수가 되었다.

Automatic Code Splitting

본래 code splitting을 하기 위해서는 React.Lazy나 dynamic import를 사용했어야했다.

import dynamic from 'next/dynamic'
            
const DynamicComponent = dynamic(() => import('../components/hello'))

하지만 RSC에서 RCC를 import 하는 케이스에서는 자동적으로 RCC를 dynamic import가 적용된다. 이 장점은 어떻게 보면 굉장히 당연한 사실인데, 서버에서 RSC가 렌더링될 때 RCC는 실행되지 않기 때문에 굳이 RCC를 즉시 import 할 필요가 없기 때문이다.

Progressive Rendering

위에서 살펴봤듯, next13부터는 컴포넌트가 서버에서 한차례 렌더링 되며, 그 결과물로 직렬화된 JSON이 생성된다고 했다. 그리고 client는 그 결과물을 스트림의 형태로 수신한다.

// Tweets.server.js
import { fetch } from 'react-fetch' // React's Suspense-aware fetch()
import Tweet from './Tweet.client'
export default function Tweets() {
  const tweets = fetch(`/tweets`).json()
  return (
    <ul>
      {tweets.slice(0, 2).map((tweet) => (
        <li>
          <Tweet tweet={tweet} />
        </li>
      ))}
    </ul>
  )
}

// Tweet.client.js
export default function Tweet({ tweet }) {
  return <div onClick={() => alert(`Written by ${tweet.username}`)}>{tweet.body}</div>
}

// OuterServerComponent.server.js
export default function OuterServerComponent() {
  return (
    <ClientComponent>
      <ServerComponent />
      <Suspense fallback={'Loading tweets...'}>
        <Tweets />
      </Suspense>
    </ClientComponent>
  )
}
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
S2:"react.suspense"
J0:["$","@1",null,{"children":[["$","span",null,{"children":"Hello from server land"}],["$","$2",null,{"fallback":"Loading tweets...","children":"@3"}]]}]
M4:{"id":"./src/Tweet.client.js","chunks":["client8"],"name":""}
J3:["$","ul",null,{"children":[["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}],["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}]]}]

위 문자열은 클라이언트가 수신하는 스트림의 한 예시를 나타낸 것이다. 여기서 짚고 넘어갈 부분은 데이터가 ‘스트림’ 형태로 전달된다는 사실이다. 즉, 스크린의 모든 화면정보를 수신할 때까지 기다릴 필요 없이, 클라이언트는 먼저 수신된 부분부터 반영하기 시작하여 화면에 띄워줄 수 있게 된다.

위 스트림 문자열을 보면 S2 지점에 suspense가 서술되어 있다. 그리고 J0를 보면 뒤쪽에 children으로 “@3”이 참조되어 있는 것을 볼 수 있다. 하지만 스트림의 어디를 봐도 “@3”에 대한 정의는 나와있지 않다. 이는, 아직 data fetch가 완료되지 않았기 때문에 fallback이 보여지는 상황이기 때문에 @3를 placehoder로 사용하고 있기 때문이다.

만약 data fetch가 완료되면 “@3”이 “J3”로 대체되고, “J3”는 참조하고 있던 “M4”에 해당하는 client component에 data를 넘겨주면서 화면에 보여지게 된다. 따라서 RSC를 React.Suspense와 함께 사용한다면 모든 데이터를 기다릴 필요 없이 먼저 그릴 수 있는 부분을 반영하여 뷰를 로드한 뒤, data fetch가 완료되면 그 결과가 즉각적으로 스트림에 반영됨을 알 수 있다.

컴포넌트 단위 refetch

전통적인 SSR의 경우 완성된 html파일을 내려주기 때문에 작은 변경사항이 발생하더라도 전체 페이지를 전부 새로 그려서 받아와야 했다. 하지만 직전에 설명했듯이 RSC는 그 최종 결과물이 html이 아니라 직렬화된 스트림 형태로 데이터를 받아오기때문에, 클라이언트에서 스트림을 해석하여 vitualDOM을 형성하고, Reconciliation을 통해 뷰를 갱신하는 과정을 거치게 된다. 즉, 화면에 변경사항이 생겨서 서버에서 새로운 정보를 받아와야하는 상황이 생기더라도, 새로운 스크린으로 갈아끼우는 것이 아니라 기존 화면의 state등 context를 유지한채로 변경된 사항만 선택적으로 반영할 수 있게 된다.

참조

https://velog.io/@brgndy/React-Server-vs-Client-Component-in-Next.js-13-해석
https://nextjs.org/docs/getting-started/react-essentials
https://www.plasmic.app/blog/how-react-server-components-work
https://patterns-dev-kr.github.io/rendering-patterns/react-server-components/
https://ui.toast.com/weekly-pick/ko_20210119![](https://velog.velcdn.com/images/2ast/post/d4e13a17-99ef-418a-ac7e-534be5a679ae/image.png)
https://ko.wikipedia.org/wiki/직렬화

profile
React-Native 개발블로그

21개의 댓글

comment-user-thumbnail
2023년 6월 8일

이해하는데 큰 도움이 됐습니다.

1개의 답글
comment-user-thumbnail
2023년 7월 10일

너무 좋은 글이네요! RSC를 쉽게 잘 정리해주신것 같아요!! 감사합니다.

1개의 답글
comment-user-thumbnail
2023년 8월 16일

잘 읽었습니다. 감사합니다!

1개의 답글
comment-user-thumbnail
2023년 11월 1일

와 대단하십니다 이글읽고 많이 배웠어요!

1개의 답글
comment-user-thumbnail
2023년 11월 22일

이거 보고 개념이 확 잡혔습니다 좋은글 감사합니다!

1개의 답글
comment-user-thumbnail
2024년 1월 3일

우와.. 너무 양질의 글이었습니다. 혹시 실례가 안된다면 출처를 남기고 제 포스팅에 참고를 해도 될까요?

1개의 답글
comment-user-thumbnail
2024년 1월 27일

RSC, RCC을 명확하게 이해할 수 있었습니다. 특히 직렬화 내용은 모르고 있었는데 이해하는 데 많은 도움이 되었어요. 실례가 안된다면 제 포스팅에 출처를 남기고 참고해도 될까요 ?

1개의 답글
comment-user-thumbnail
2024년 3월 6일

좋은글 너무 잘 읽었습니다! 혹시 괜찮으시다면 출처를 남기고 그림과 글을 인용해도 될까요??

1개의 답글
comment-user-thumbnail
2024년 3월 19일

좋은 글 너무 감사합니다. 혹시 모든 출처를 남기고 해당 글을 다시 제 블로그에 정리해봐도 될까요??

1개의 답글
comment-user-thumbnail
2024년 4월 7일

멋진 글 작성해주셔서 감사합니다!! rsc를 이해하는데 도움이 많이 됐어요
https://velog.io/@2ast/React-children-prop에-대한-고찰feat.-렌더링-최적화
이 글도 감명깊에 봤는데 이 글의 저자셨군요.. 정말 대단하십니다 항상 좋은 글 작성해주셔서 감사해요bb

1개의 답글