Skip to content

Conversation

@piggggggggy
Copy link

@piggggggggy piggggggggy commented Nov 15, 2025

과제 체크포인트

배포 링크

https://piggggggggy.github.io/front_7th_chapter2-2/

기본과제

Phase 1: VNode와 기초 유틸리티

  • core/elements.ts: createElement, normalizeNode, createChildPath
  • utils/validators.ts: isEmptyValue
  • utils/equals.ts: shallowEquals, deepEquals

Phase 2: 컨텍스트와 루트 초기화

  • core/types.ts: VNode/Instance/Context 타입 선언
  • core/context.ts: 루트/훅 컨텍스트와 경로 스택 관리
  • core/setup.ts: 컨테이너 초기화, 컨텍스트 리셋, 루트 렌더 트리거

Phase 3: DOM 인터페이스 구축

  • core/dom.ts: 속성/스타일/이벤트 적용 규칙, DOM 노드 탐색/삽입/제거

Phase 4: 렌더 스케줄링

  • utils/enqueue.ts: enqueue, withEnqueue로 마이크로태스크 큐 구성
  • core/render.ts: render, enqueueRender로 루트 렌더 사이클 구현

Phase 5: Reconciliation

  • core/reconciler.ts: 마운트/업데이트/언마운트, 자식 비교, key/anchor 처리
  • core/dom.ts: Reconciliation에서 사용할 DOM 재배치 보조 함수 확인

Phase 6: 기본 Hook 시스템

  • core/hooks.ts: 훅 상태 저장, useState, useEffect, cleanup/queue 관리
  • core/context.ts: 훅 커서 증가, 방문 경로 기록, 미사용 훅 정리

기본 과제 완료 기준: basic.equals.test.tsx, basic.mini-react.test.tsx 전부 통과

심화과제

Phase 7: 확장 Hook & HOC

  • hooks/useRef.ts: ref 객체 유지
  • hooks/useMemo.ts, hooks/useCallback.ts: shallow 비교 기반 메모이제이션
  • hooks/useDeepMemo.ts, hooks/useAutoCallback.ts: deep 비교/자동 콜백 헬퍼
  • hocs/memo.ts, hocs/deepMemo.ts: props 비교 기반 컴포넌트 메모이제이션

구조 및 구현 소개

 packages/react/
  ├── src/
  │   ├── core/                    # 핵심 렌더링 엔진
  │   │   ├── commit.ts           # DOM 변경 커밋
  │   │   ├── constants.ts       
  │   │   ├── context.ts          # 전역 Singleton Context
  │   │   ├── dom.ts              # DOM 조작
  │   │   ├── elements.ts         # Element 생성
  │   │   ├── hookManager.ts      # Component 실행
  │   │   ├── hooks.ts            # 기본 React Hooks
  │   │   ├── reconciler.ts       # React Reconciler
  │   │   ├── render.ts           # 렌더링
  │   │   ├── scheduler.ts       
  │   │   ├── setup.ts            # 초기 설정
  │   │   ├── types.ts            # 타입 정의
  │   │   └── index.ts
  │   │
  │   ├── hooks/                
  │   │   ├── useAutoCallback.ts
  │   │   ├── useCallback.ts
  │   │   ├── useDeepMemo.ts
  │   │   ├── useMemo.ts
  │   │   ├── useRef.ts
  │   │   ├── types.ts
  │   │   └── index.ts
  │   │
  │   ├── hocs/                    
  │   │   ├── memo.ts           
  │   │   ├── deepMemo.ts  
  │   │   └── index.ts
  │   │
  │   ├── utils/                
  │   │   ├── enqueue.ts 
  │   │   ├── equals.ts     
  │   │   ├── validators.ts
  │   │   └── index.ts
 ...

전반적인 구조와 동작의 흐름은 다음과 같습니다 :

  1. VNode 생성 (Virtual DOM)
  2. 초기 렌더링 (Setup & First Render)
  3. Render Phase (Reconciliation)
  4. Commit Phase (실제 DOM 조작)
  5. Hooks (useState & useEffect)
  6. Scheduler (비동기 Baching 렌더링)
  7. 사후 처리 (Cleanup & Effects)

1. VNode 생성 (Virtual DOM)

흐름의 시작은 개발자가 작성한 JSX를 Babel이 createElement 함수로 변환시키면서 시작됩니다.
최종적으로 createElementVNode 객체를 만들어냅니다.

{
  type: 'div',
  props: { className: 'button', children: [...] },
  key: null,
}
...
{
  type: ProductCard, // function
  props: { productId: '123', title: '뚫어뻥', count: 5, ... },
  key: 'product-3',
}
...
{
  type: Fragment, // Symbol
  props: { productId: '123', title: '뚫어뻥', count: 5, ... },
  key: 'product-3',
}
...
{
  type: Text, // Symbol
  props: { nodeValue: '텍스트', children; [...] },
  key: null,
}

이렇게 createElement 를 통해 다양한 형태의 요소들을 일관된 VNode 형태로 정규화합니다. 이 덕에 이후 로직은 정규화된 VNode 객체를 통해 진행됩니다.

2. 초기 렌더링 (Setup & First Render)

앱이 시작될 때, setup() 함수가 호출됩니다.
setup() 함수의 역할을 다음과 같습니다.

  1. 초기화 : Context에 있는 전역 상태들(Root Container, Hooks 관련 정보, Effects 등)을 정리합니다.
  2. 첫 렌더링 트리거 : render() 함수를 호출하여 첫 렌더링 작업을 시작합니다.

3. Render Phase (Reconciliation)

render() 함수 호출 후, 이 mini React의 핵심 단계인 재조정(Reconciliation) 단계가 시작됩니다. 이 과정은 다음과 같은 철학을 갖습니다.

"변경된 부분만 찾아서 업데이트한다."

reconcile 함수는 이를 위해

  1. 비교 (Diffing)
  2. 비교 결과에 따른 4가지 시나리오
  3. 컴포넌트 실행
    의 순서로 작업을 진행합니다.

A. 비교 (Diffing)

변경된 부분만 찾아내기 위해 다음 세가지 인자를 받습니다.

  • 부모 DOM : 변경 사항을 반영할 실제 DOM 요소
  • 기존 인스턴스 (Old Instance) : 직전 렌더링 때 생성된 정보 (실제 DOM 참조, 기존 Props 등 포함)
  • 새로운 VNode : 이번 렌더링으로 그려져야 할 모습

B. 비교 결과에 따른 4가지 시나리오

Dffing 비교 결과에 따라,

  • Unmount (제거)
    • 상황 : 새로운 VNode가 없음 (null임)
    • 액션 : REMOVE effect를 예약
  • Mount (생성)
    • 상황 : 기존 인스턴스가 없음
    • 액션 : 새로운 인스턴스를 생성하고, INSERT effect(domMutation)를 예약
  • Replace (교체)
    • 상황 : VNode의 type이나 key가 다름
    • 액션 : 완전히 다른 요소로 간주, 기존 요소를 REMOVE하고 새로운 요소를 INSERT 하는 effect를 예약
  • Update (갱신)
    • 상황 : VNode의 typekey가 같음
    • 액션 :
      • DOM 재사용 : 기존 DOM 유지
      • Props 갱신 : UPDATE_PROPS effect를 예약하여 속성만 교체
      • 위치 보정 : INSERT effect를 예약하여 순서가 바뀌었다면 제자리를 찾아줌
      • 자식 재귀 호출 : reconcileChildren을 통해 자식들의 비교를 재귀적으로 수행
// reconcilers.ts

...
  // 1. Unmount
  if (!node) {
    if (instance) handleUnmount(parentDom, instance);
    return null;
  }

  // 2. Mount
  if (!instance) {
    return handleMount(parentDom, node, path, anchor);
  }

  // 3. Replace
  if (instance.node.type !== node.type || instance.key !== node.key) {
    return handleReplace(parentDom, instance, node, path, anchor);
  }

  // 4. Update
  return handleUpdate(parentDom, instance, node, path, anchor);
...

C. 컴포넌트 실행

VNode의 typeFunction Component일 때,

  • hookManager.runComponent()를 통해 실행
  • 이때, Hooks가 실행됨. (useState, useEffect)
  • 컴포넌트 실행 결과로 나온 VNode를 가지고 다시 자식 reconciliation 을 시작!!

4. Commit Phase (실제 DOM 조작)

재조정(reconciliation) 과정은 실제 DOM을 건드리지 않고, "무엇을 해야할지?"(domMutation)만 mutationQueue에 추가합니다. 재조정이 끝나면, 그 때 domMutations commit이 시작됩니다.

-> commitMutations(runtimeContext.workQueue.domMutations) : queue에 추가된 Effects(INSERT, REMOVE, UPDATE_PROPS 등)를 순서대로 실행하여 한번에 실제 DOM에 반영합니다.

5. Hooks 동작 원리 (useState & useEffect)

A. 전역 Context

컴포넌트에 작성되는 Hooks과 관련된 상태/정보를 각 컴포넌트의 맥락에 저장하지 않고, 전역의 Context라는 전역 저장소에 저장합니다. Key(component path) : Value(state[], effect[]) 형태로 저장.

B. Cursor 기반 접근

컴포넌트가 실행될 때, 내부의 훅들은 순서대로 실행됩니다.

예시 :

  1. useState 호출 -> state 배열의 0번 인덱스로 저장
  2. useEffect 호출 -> effects 배열의 0번 인덱스로 저장
  3. 두번째 useState 호출 -> state 배열의 1번 인덱스로 저장
    (+) 원래 state와 effects를 따로 저장하고, 각각의 Cursor도 따로 운용했으나, 리팩터링하며 하나의 Hooks Map에 저장하고 Cursor도 하나로 통일하여 운용하는 것으로 변경

막간 A-ha 모먼트
-> 이래서 React Hooks은 조건문 안에서 실행되면 안된다는 거구나..! 조건 문에 의해 Cursor index가 꼬이면 의도지 않은 상태나 이펙트를 참조하게 될 수 있으니간..!!

C. 상태 변경 (setState)

setState가 호출되면 :

  1. 전역 저장소(Context)에 있는 값을 업데이트
  2. enqueueRender()를 호출하여 리렌더링을 예약!

6. Scheduler (비동기 Baching 렌더링을 통한 성능 최적화)

setState를 5번 연속 호출한다고 해서 render가 5번 발생하지 않음.

  • enqueueRender() : queueMicrotask를 사용해 렌더링 작업을 마이크로태스크 �큐에 넣음
  • Batching : 자바스크립트 콜 스택이 비워질 때까지 기다렸다가, 한 번에 가장 최신의 상태로 딱 한번만 render()를 실행시킴.

7. 사후 처리 (Cleanup & Effects)

렌더링과 DOM 업데이트가 모두 끝난 직후 :

  1. Cleanup: 언마운트된 컴포넌트나, 의존성이 바뀐 Effect의 Cleanup 함수를 실행함.
  2. useEffect 실행 : 새로 등록된 Effect 함수들을 실행함.

전체 동작 요약

  1. Trigger : setState 호출 -> 상태 업데이트 -> enqueueRender 예약
  2. Redner 시작 : 스케쥴러에 의해 render() 실행
  3. Hooks Reset : Cursor를 0으로 초기화
  4. Reconciliation :
  • Component 함수 실행
  • 새로운 VNode 생성
  • 이전 VNode 비교 (Diffing)
  • 변경 사항을 domMutations 큐에 추가
  1. Commit : domMutations 큐를 실행하여 화면 갱신
  2. Effects : cleanup 실행 후 새로운 useEffect 실행

개선

기존의 단일 Context 객체를 운용하는 방식으로 기능 구현은 성공했지만, **"데이터 흐름"**과 **"제어의 흐름"**이 명확하게 분리되어있지 않다고 느껴졌습니다

Render 의 흐름이 눈에 명확하게 들어오지 않는다고 느껴졌습니다.

결론적으로 이런 개선이 이뤄졌습니다.

  1. 투명한 데이터 흐름 (render.ts) - render 함수가 파이프라인 역할을 하는 것처럼 보이도록 !!!
  2. 영속 Context와 일시적 Context 분리 (context.ts) - storeContextruntimeContext로 분리!
  3. 역할의 명확화 (commit.ts, scheduler.ts) - reconcile함수는 이제 **"계산"**에만 집중하고, 반영은 전담 모듈이!!!

이제 reconcile 함수의 내부 로직이 복잡하더라도, 막상 그 역할은 domMutations Queue를 채우는 역할일 뿐이다!!

1. Context 구조 개선

Context의 구조를 **"영속 상태"**와 **"일시적 상태와 작업"**으로 분리하는 것으로 방향을 잡았습니다.

context 객체에 모든 상태와 정보를 관리하는 것 대신,

  1. StoreContext (영속) : 렌더링이 끝나도 기억해야 하는 값들 - Hook 상태, 현재 VDOM, Instance 등
  2. RuntimeContext (일시적) : 렌더링 중에만 사용되고, 매 프레임마다 초기화되는 값들 - 커서, 큐 등
// ASIS
const context: Context = {
  root: {
    container: null,
    node: null,
    instance: null,
    reset({ container, node }) {...},
  },

  hooks: {
    state: new Map(),
    cursor: new Map(),
    visited: new Set(),
    componentStack: [],
    clear() {...},

    // Method ..
    get currentPath() {...},

    get currentCursor() {...},

    get currentHooks() {...},
  },

  effects: {
    queue: [],
  },

  domEffects: {...}
};

// TOBE

// Persistent context
export const storeContext: StoreContext = {
  root: {
    container: null,
    node: null,
    instance: null,
  },
  hooks: new Map(),
  cleanupEffects: new Map(),
};

// Temporary context for each render
export const runtimeContext: RuntimeContext = {
  cursor: {
    path: null,
    index: 0,
  },
  workQueue: {
    domMutations: [],
    passiveEffects: [],
    cleanups: [],
  },
  componentStack: [],
  visited: new Set(),
};

export const resetRuntime = () => {
  runtimeContext.cursor.path = null;
  runtimeContext.cursor.index = 0;
  runtimeContext.workQueue.domMutations = [];
  runtimeContext.workQueue.passiveEffects = [];
  runtimeContext.workQueue.cleanups = [];
  runtimeContext.visited.clear();
};

2. Render Pipline 개선

render 함수가 전체 생명주기의 목차역할을 할 수 있도록 추상화 및 정리를 진행했습니다.

render 함수만 봐도
준비 -> 재조정 -> 반영 -> 이펙트의 흐름이 한눈에 들어올 수 있도록 구성했습니다.

const ROOT_PATH = "root";

export const render = (): void => {
  // 1. reset runtime context
  resetRuntime();

  // 2. reconcile
  const oldInstance = storeContext.root.instance;
  const newInstance = reconcile(
    storeContext.root.container as HTMLElement,
    oldInstance,
    storeContext.root.node,
    ROOT_PATH,
  );
  storeContext.root.instance = newInstance;

  // 3. Unused Hooks Cleanup
  cleanupUnusedHooks();

  // 4. commit mutations
  commitMutations(runtimeContext.workQueue.domMutations);

  // 5. flush passive effects
  enqueue(() => {
    flushPassiveEffects(runtimeContext.workQueue.passiveEffects, runtimeContext.workQueue.cleanups);
  });
};

3. Commit 로직의 분리와 투명화

reconcile 내부에서 domEffects 를 추가하는 구조로 디자인했지만, 수집된 domEffect 들이 어떻게 처리되는지 를 매끄럽게 확인하기 어려웠습니다.
따라서, 이것이 어떻게 처리되는지를 commit.ts 모듈로 분리했습니다.

"reconcile"은 계산을 통해 Mutation Queue를 추가하고, "commit"은 이 Queue를 보고 Mutation 작업을 집행한다. 는 컨셉

// commmit.ts
import { updateDomProps, insertInstance, removeInstance } from "./dom";

export const commitMutations = (mutations: DomEffect[]) => {
  for (const mutation of mutations) {
    switch (mutation.type) {
      case "INSERT":
        insertInstance(mutation.parentDOM, mutation.instance, mutation.anchor);
        break;
      case "REMOVE":
        removeInstance(mutation.parentDOM, mutation.instance);
        break;
      case "UPDATE_PROPS":
        updateDomProps(mutation.dom, mutation.prevProps, mutation.nextProps);
        break;
      case "UPDATE_TEXT":
        mutation.dom.nodeValue = mutation.nextText;
        break;
    }
  }
};

4. Effect Scheduler 정리

기존의 복잡했던 이펙트 처리(old 정리 후 new 실행) 로직을, scheduler라는 이름으로 묶어 명확히 수행하는 방향으로 개선했습니다.

// scheduler.ts

export const flushPassiveEffects = (newEffects: EffectHook[], unmountCleanups: (() => void)[]) => {
  // 1. unmount components
  unmountCleanups.forEach((cleanup) => cleanup());

  // 2. execute previous cleanup functions
  newEffects.forEach((effect) => {
    // execute previous cleanup functions
    const cleanups = storeContext.cleanupEffects.get(effect.path);
    if (cleanups) {
      cleanups.forEach((cleanup) => cleanup());
      storeContext.cleanupEffects.delete(effect.path);
    }

    if (effect.cleanup) {
      effect.cleanup();
      effect.cleanup = null;
    }
  });

  // 3. new!
  newEffects.forEach((effect) => {
    try {
      const cleanupFn = effect.effect();
      if (typeof cleanupFn === "function") {
        effect.cleanup = cleanupFn;
      } else {
        effect.cleanup = null;
      }
    } catch (e) {
      console.error(e);
    }
  });
};

과제 회고

작업의 흐름과 고민들...

실제 작업 시간보다는 공부하고 이해하는 시간이 월등히 길었습니다. 결국에는 Mini React를 구현하는 과정이기 때문에, React의 구조에서 사고를 해야한다고 생각했는데, 그렇다보니 실제 React의 동작원리와 철학에 대한 이해 없이는 시작하기가 너무 어려웠습니다.

처음에는 내가 이 과제를 통해 구현해야하는 것은 무엇인가? 라는 고민부터 시작했습니다.

  1. React 구현
  2. React가 하는 것은 뭐지?
  3. 그 부분에서도 내가 구현해야하는 것은?
  4. React의 SPA철학은? 다른 SPA 도구들과의 차이점은?
  5. React를 직접 구현하기 앞서, React 밖에서 내가 개념이 부족한 부분이 있는지?
    등의 고민과 학습이 선행되었고... 작업 계획을 짰습니다.

createElement

구현은 createElement의 구현부터 시작했습니다.
처음에 상당히 어렵고 답답했는데, "내가 왜 이걸 하고 있지? 이게 필요한 이유는 뭐지? 뭐가 이해 안되는거지?" 이런 고민을 하다보니, JSX와 JSX가 변환되는 과정에 대한 이해가 부족하다는 것을 깨닫고, 실제 JSX가 Babel을 통해 어떻게 변환이 되는지 부터 분석했습니다. 그과정에서 Babel Playground를 찾아내 아주 많은 도움을 받았습니다.

그 과정에서
"그냥 배열, 같은 빈 객체 타입이나, 원시타입은 어떤 형태의 VNode로 만들어져야할까?"
"createElement가 VNode로 변환하는 과정에서 []이나 {}는 어떻게 처리할까? 그리고 빈 객체가 아닌 채워진 객체라면?"
"Fragment는 전용 vnode가 있다. 이것을 돔을 수정할 때 계산한다. -> 왜?"
이런 질문을 던지고, 해소하는데도 적잖은 시간을 썼습니다..

구조 개념잡기 (Context, Instance, Virtual DOM, Real DOM)

"React는 Virtual DOM을 활용하여, 변경된 부분만 Real DOM에 반영한다." 정도는 이제 FE개발자로서,, 알기는 합니다만, React를 실제로 구현하는 과정에서의 Virtual DOM을 이해하는 것은 아예 다른 차원의 이해였습니다.

"왜 Virtual DOM을 사용하나, VIrtual DOM의 역할은?" 부터 시작해. React 엔진의 원리와 구조를 코드 레벨에서 이해애야했습니다.

  • Virtual DOM
  • Real DOM
  • Instance
  • Context

동작 개념잡기 (Setup, Render, Reconcile, DOM 조작 그리고 Life Cycle)

내부에 존재하는 함수들과, 그 역할 그리고 실제 각 라이프 사이클에서 어떤 파이프라인으로 동작하는지 이해하는데도 적잖은 시간을 사용했습니다.

이 과정에서 정말 많은 역할들이 있지만, 그게 "정말 필요해서 있다"를 이해하기 까지 머리가 너무 아팠습니다. 정규화된 VNode를 기존 인스턴스의 VNode와 비교하고,, 그것을 또 재귀적으로 수행하고,, 그 과정에서 Instance의 dom에 붙이고,, 그러다보니 더 헷갈려서, reconcile에서 dom 조작의 관심사를 아예 분리해서 domEffect (현 domMutation) 과정을 새로 만들어내고.... 뇌가 무한 재귀에 빠졌다가 나왔다가를 반복하는 시간이었습니다.

실제 작성했던 고민들

Q) render 함수 안에서, context를 가지고 reconcile함수를 돌린다면, 그리고 reconcile에서 instance를 업데이트한다면, dom 업데이트는 어디서 이뤄져야할까? render? reconcile?

Q) reconcile과 commit을 분리했을 때, reconcile과정에서 Instance Tree의 VNode는 업데이트 되고있음. 그런데 Instance가 자체가 교체되면(업데이트가 아닌), dom도 날아갈텐데, reconcile된 instance tree로 어떻게 commit을 할까? reconcile 과정에서 create instance 할 때, dom도 만들어줘야할까?

"Component는 DOM이 없는데 instance.dom은?"
"자식의 DOM을 참조한다는데, 자식이 여러 개면?"
"Reconcile 중 Instance가 교체되면 dom 참조가 끊기는데?"

useStateuseEffect, cursor

아... 지금 생각해도 아찔합니다. 단순히 지난주차의 수준이 아니었습니다ㅠㅠ 지난주에 집중했던 포인트는, state의 변경이 어떻게 컴포넌트의 리렌더를 트리거할 것 인지?에 집중했다면, 이번에는 그 사고를 React 엔진의 관점으로 확장해야했습니다. 상태 값과 deps의 저장, 다음 렌더 시 그 값을 어떻게 다시 찾아내 참조할 것인지, Cursor가 정답일까? 뭔가 조악한데... 등의 고민이 핵심이었던 것 같습니다.

처음에는 Cursor와 Hook Map을 stateeffect 따로 운용했지만, 나중에는 위 과정에서의 이해를 통해 이를 하나의 Cursor와 Hooks로 통합 관리할 수 있게 되었습니다...

아하! 모먼트 (A-ha! Moment)

굉장히 많은 아-하! 모먼트가 있었지만, 하나하나 작성하기에는 머리가 아픕니다..

  1. 막연하게만 알고 있던, "hook은 왜 조건문 안에서 사용하면 안될까?"를 명확하게 알게 되었습니다.
    직접 Cursor 기반의 Hook Context 관리 구조를 이해하고 디자인 하면서, 조건부 호출 때문에 Cursor Index가 오염된다면, 의도치 않은 상태와 이펙트가 사용될 수 있겠구나! 이해할 수 있었습니다.

  2. 심화 과제에서 추가 훅을 구현하는 과정이 대부분 아-하! 모먼트였던 것 같습니다. 기존에는 hook을 사용할 때, "이건 이런 상황에 사용해야해", "이 훅은 이런 부분을 메모이제이션 해줘!" 수준의 이해에서 그쳤다면, "이건 이런 메모이제이션을 하는 훅이니까(기존 지식 수준), 이 React의 구조에서는 이런 부분이 캐싱이되면 실제로 기대하는대로 동작할 수 있겠다!"로 확장되는 경험을 했습니다. (적고나니 무슨말인가 싶긴하네요..)

  3. 생각나면 추가하겠습니다...

학습과 성장

과제를 마친 지금 시점에서 생각해보면,,, 기대 이상의 학습과 경험이었습니다.

내가 무에서 유를 만든 것은 아니고, 이미 잘 만들어진 구조를 재건축해보는 일을 한 것일 뿐이지만..
그렇기에 명확한 큰 목표 아래서 굉장히 밀도있게 문제 해결을 해보고 그것을 잘 돌아가는 하나의 시스템으로 만들어내기 위해 다양한 고민과 경험을 해봤다는 느낌을 받습니다. 특히 심화 과제의 훅들을 만들고 테스트하는 과정에서 그전의 과정과 달리 빠른 시간안에 문제를 해결하고 테스트가 통과되는 경험을 했고, 이 경험이 "내가 잘 만들어 냈구나!" 라는 효능감이 폭발했던 시점이었습니다.

과제의 성공을 떠나서, 이번 경험을 통해 얻은 가장 큰 통찰은
"React도, Component도, Virtual DOM도, Hook들도 결국에는 Magic이 아닌 문제해결의 결과물이구나" 라는 생각이었습니다. 오랫동안 React나 Vue같은 도구 위에서 작업하다보니 굳어진 사고의 울타리가 깨지는 좋은 경험이었습니다.

리뷰 받고 싶은 내용

@piggggggggy piggggggggy changed the title init [1팀 박용태] Chapter2-2. 나만의 React 만들기 Nov 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant