항해 플러스 7기 5주차

드엔트론프·2025년 11월 23일

항해플러스

목록 보기
5/9
post-thumbnail

5주차

지난주 바닐라 JS를 SPA로 만드는 것 이후, 이제는 리액트 만들기에 들어왔다.

  • 크게 하드, 이지로 나누어졌는데, 나는 이지의 기본 / 심화 통과를 목표로 시작했다. 처음에는 무조건 하드로 시작하면서 해야지 하는 마음을 갖고 임했었는데, 앞서 살펴본 결과 하드하면 이도저도 아니게 되어버린 주가 있었기에..애초에 학습을 목적으로 하는 프로그램인데 하나라도 차근차근 내 것을 만들어가자는 마음으로 시작했다.

Keep

흐름을 따라가며 이해해보려 노력한 일

  • 처음에는 어떤 식으로 시작해야할 지 감이 안왔다.

  • 코치님이 '테스트 하나하나 통과하는 느낌으로 진행'해보라는 말이 있었다.

  • 발제 노트에서 가상돔 구현하기를 작성해 주신 내용이 있어서 참고하며 읽는데 뭐야 그냥 저대로 쓰면 되는거 아니야? 꿀이다! 하고 사용하려했다.

  • 근데 사용 후 테스트코드 돌렸더니 자꾸 터지는거.

  • 역시 그대로 작성하는 게 아니었다 ㅎㅎ;

  • 아래에 이해하려 정리한 내용들은 길어서 토글로 만들어두었다.

CreateVNode

정리

처음 막혔던 부분은 children이 flat하게 그려져야 하는 부분.

export function createVNode(type, props, ...children) {
  return { type, props, children: children.flat() };
}

flat() 은 아무 내용 없이 쓰면 그냥 1단계만 평탄화 시켜준다.

근데 flat(Infinity) 써주면 전부 평탄화시켜줌

그래서 끝난줄 알았는데 2번째 터진 부분은 예외처리 부분이다.

describe("JSX로 표현한 결과가 createVNode 함수 호출과 동일해야 한다", () => {
      const TestComponent = ({ message }) => <div>{message}</div>;
      const ComplexComponent = ({ items, onClick }) => (
        <div className="container">
          {items.map((item) => (
            <span key={item.id}>{item.text}</span>
          ))}
          <button onClick={onClick}>Click me</button>
        </div>
      );

      it.each([
	       {
          name: "조건부 렌더링",
          vNode: (
            <div>
              {true && <span>Shown</span>}
              {false && <span>Hidden</span>}
            </div>
          ),
          expected: {
            type: "div",
            props: null,
            children: [{ type: "span", props: null, children: ["Shown"] }],
          },
        },
        ..
        ..
        ..
      )]
  • 여기 true일 때는 보이고, false는 아예 그려지면 안되는데, 앞서 flat(Intinity) 만으로는 해결이 안됨.
  • 이건 필터 처리해보는 게 어떨까?
export function createVNode(type, props, ...children) {
  const flatFilterChildren = children
    .flat(Infinity)
    .filter(
      (child) => child !== null && child !== undefined && child !== false
    );
  return { type, props, children: flatFilterChildren };
}

필터로 null, undefined, false 를 솎아냈다!
이렇게 createVNode를 지나갈 수 있었다.

normalizeVNode

정리
  • 이 친구는 vNode를 정규화한다. 라고 명시돼있다.
  • vNode를 정규화한다는 건 무슨 의미일까?
  • 정규화란?
    • 어떤 대상을 일정한 규칙이나 기준에 따르는 '정규적인' 상태로 바꾸거나, 비정상적인 대상을 정상적으로 되돌리는 과정
  • 그러니까, 지멋대로 보여질 수도 있는 내용을 일정한 규칙을 통해 동일한 형태로 바꿔준다고 생각하면 될 것 같다.
  • 정규화에서 제일 쉽지 않은건 컴포넌트를 정규화 한 일
    • 재귀에 대해 고민해야한다.
// 컴포넌트를 정규화한다 
const UnorderedList = ({ children, ...props }) => (
  <ul {...props}>{children}</ul>
);
const ListItem = ({ children, className, ...props }) => (
  <li {...props} className={`list-item ${className ?? ''}`}>
    - {children}
  </li>
);
const TestComponent = () => (
  <UnorderedList>
    <ListItem id="item-1">Item 1</ListItem>
    <ListItem id="item-2">Item 2</ListItem>
    <ListItem id="item-3" className="last-item">
      Item 3
    </ListItem>
  </UnorderedList>
);

JSX → createVNode 함수 호출

<TestComponent />
{
  type: TestComponent,  // 함수
  props: null,
  children: []
}

normalizeVNode에 전달

normalizeVNode(<TestComponent />)
// normalizeVNode({ type: TestComponent, props: null, children: [] })

normalizeVNode 내부 처리 - 컴포넌트 함수 실행

  // 함수면, 재귀로 돌려버림
  if (typeof vNode.type === 'function') {
    return normalizeVNode(
      vNode.type({ ...vNode.props, children: vNode.children }) // --> TestComponent({}) 가 실행됨! 
    );
  }

TestComponent 실행되면 아래처럼 나오는데,

<UnorderedList>
  <ListItem id="item-1">Item 1</ListItem>
  <ListItem id="item-2">Item 2</ListItem>
  <ListItem id="item-3" className="last-item">Item 3</ListItem>
</UnorderedList>

이게 JSX이니까 createVNode 호출들이 연쇄적으로 일어남

{
  type: UnorderedList,  // 함수
  props: {},
  children: [
    { type: ListItem, props: { id: 'item-1' }, children: ['Item 1'] },
    { type: ListItem, props: { id: 'item-2' }, children: ['Item 2'] },
    { type: ListItem, props: { id: 'item-3', className: 'last-item' }, children: ['Item 3'] }
  ]
}

UnorderedList 컴포넌트 함수 UnorderedList({ children:[3개 ListItem 있는 배열]}) 실행

  // 함수면, 재귀로 돌려버림
  if (typeof vNode.type === 'function') {
    return normalizeVNode(
      vNode.type({ ...vNode.props, children: vNode.children }) 
      // --> UnorderedList({ children:[3개 ListItem 있는 배열]}) 가 실행됨! 
    );
  }

다시 normalizeVNode가 호출되면, UnorderedList도 함수니까 실행되고:

<ul {...{}}>{children}</ul>

JSX를 반환

{
  type: 'ul',  // ← 문자열! (이제 DOM 엘리먼트)
  props: {},
  children: [
    { type: ListItem, props: { id: 'item-1' }, children: ['Item 1'] },
    { type: ListItem, props: { id: 'item-2' }, children: ['Item 2'] },
    { type: ListItem, props: { id: 'item-3', className: 'last-item' }, children: ['Item 3'] }
  ]
}

type이 문자열이면 children 정규화

  // nomalizeVNode 조건 중
  
  if (typeof vNode.type === 'string') {
    // children 배열을 정규화하고 빈 문자열 제거
    const normalizedChildren = vNode.children
      .map((child) => normalizeVNode(child)) // --> ListItem 정규화 됨.
      .filter((child) => child !== '');

    return {
      ...vNode,
      children: normalizedChildren
    };
  }

각 ListItem이 정규화. ListItem도 함수

ListItem 컴포넌트 함수 실행

ListItem({ id: 'item-1', children: ['Item 1'] })

반환

<li {...{ id: 'item-1' }} className={`list-item ${null ?? ''}`}>
  - {children}
</li>

JSX 결과

{
  type: 'li',  // ← 문자열
  props: { id: 'item-1', className: 'list-item ' },
  children: [
    '- ',
    'Item 1'
  ]
}

li의 children 재귀 정규화

// nomalizeVNode 조건 중
  
  if (typeof vNode.type === 'string') {
    // children 배열을 정규화하고 빈 문자열 제거
    const normalizedChildren = vNode.children // --> children:  ['- ', 'Item 1']
      .map((child) => normalizeVNode(child)) 
      .filter((child) => child !== '');

    return {
      ...vNode,
      children: normalizedChildren
    };
  }
  // 문자열과 숫자는 문자열로 변환
  if (typeof vNode === 'string' || typeof vNode === 'number') {
    return String(vNode); // --> 그대로 문자열 반환 '- ', 'Item 1'
  }

최종 li

{
  type: 'li',
  props: { id: 'item-1', className: 'list-item ' },
  children: ['- ', 'Item 1']  // 두 개의 문자열
}

최종 결과

{
  type: 'ul',
  props: {},
  children: [
    { type: 'li', props: { id: 'item-1', className: 'list-item ' }, children: ['- ', 'Item 1'] },
    { type: 'li', props: { id: 'item-2', className: 'list-item ' }, children: ['- ', 'Item 2'] },
    { type: 'li', props: { id: 'item-3', className: 'list-item last-item' }, children: ['- ', 'Item 3'] }
  ]
}

createElement

정리
  • 이게 어떻게 createElement가 처리할까?

최상위 <ul> 처리

  createElement({
    type: 'ul',
    props: {},
    children: [
      { type: 'li', props: { id: 'item-1', className: 'list-item ' }, children: ['- ', 'Item 1'] },
      { type: 'li', props: { id: 'item-2', className: 'list-item ' }, children: ['- ', 'Item 2'] },
      { type: 'li', props: { id: 'item-3', className: 'list-item last-item' }, children: ['- ', 'Item 3'] }
    ]
  })
  // createElement
  if (
	  typeof vNode === 'object' &&           // ✅ true (객체)
	  vNode.type &&                          // ✅ true ('ul')
	  typeof vNode.type === 'string'         // ✅ true (문자열)
) {
  const $element = document.createElement(vNode.type);  // <ul></ul> DOM 생성

<ul>의 props 설정

// createElement   
   
    if (vNode.props) {  
      Object.entries(vNode.props).forEach(([key, value]) => {
        const attrName = key === 'className' ? 'class' : key;

        if (attrName.startsWith('on')) {
          const eventType = attrName.slice(2).toLowerCase();
          addEvent($element, eventType, value);
        } else {
          setElementProperty($element, key, value);
        }
      });
    }
  • props = {}이니까 빈 객체
  • Object.entries 돌지 않음

<ul>의 자식 처리 (재귀 시작)

if (vNode.children) {  // ✅ 자식이 3개 있음
  vNode.children.forEach((child) => {
    $element.appendChild(createElement(child));
    // 각 <li> vNode를 createElement로 호출
  });
}

첫 번째 자식 <li id="item-1">:

createElement({
  type: 'li',
  props: { id: 'item-1', className: 'list-item ' },
  children: ['- ', 'Item 1']
})
  // createElement
  
  if (
    typeof vNode === 'object' &&
    vNode.type &&
    typeof vNode.type === 'string'
  ) {
    const $element = document.createElement(vNode.type); // --> <li></li> 만듦
    
    if (vNode.props) {
      Object.entries(vNode.props).forEach(([key, value]) => {
        const attrName = key === 'className' ? 'class' : key;

        if (attrName.startsWith('on')) {
          const eventType = attrName.slice(2).toLowerCase();
          addEvent($element, eventType, value);
        } else {
          setElementProperty($element, key, value);
        }
      });
    }
  • props가 있다. id: 'item-1', className: 'list-item '

이는 아래처럼 동작하게된다.

// createElement 83-94줄
if (vNode.props) {  // { id: 'item-1', className: 'list-item ' }
  Object.entries(vNode.props).forEach(([key, value]) => {
    // 첫 번째 반복: key = 'id', value = 'item-1'
    const attrName = key === 'className' ? 'class' : key;  // attrName = 'id'
    
    if (attrName.startsWith('on')) {  // ✅ false ('id'는 'on'으로 시작 안 함)
      // 이벤트 핸들링 안 함
    } else {
      setElementProperty($element, 'id', 'item-1');  // 13-39줄 실행
      // → $element.setAttribute('id', 'item-1')
      // → <li id="item-1"></li>
    }
  });
  
  // 두 번째 반복: key = 'className', value = 'list-item '
  Object.entries(vNode.props).forEach(([key, value]) => {
    const attrName = key === 'className' ? 'class' : key;  // attrName = 'class'
    
    if (attrName.startsWith('on')) {  // ✅ false
      // 이벤트 핸들링 안 함
    } else {
      setElementProperty($element, 'className', 'list-item ');
      // 13-14줄: className → class로 변환
      // → $element.setAttribute('class', 'list-item ')
      // → <li id="item-1" class="list-item "></li>
    }
  });
}

<li id="item-1" class="list-item "></li> 완성

<li>의 자식 처리

// 97-101줄
if (vNode.children) {  // ['- ', 'Item 1']
  vNode.children.forEach((child) => {
    $element.appendChild(createElement(child));
  });
}

첫 번째 자식 '- ' (문자열):

createElement('- ')
// createElement 52 ~ 54
if (typeof vNode === 'string' || typeof vNode === 'number') {  // ✅ true
  return document.createTextNode('- ');  // 텍스트 노드 반환
}

→ 텍스트 노드 '- ' 생성 후 <li>에 append

$element.appendChild(document.createTextNode('- '));
// <li id="item-1" class="list-item ">- </li>

두 번째 자식 'Item 1' (문자열):

createElement('Item 1')
// createElement 52 ~ 54
if (typeof vNode === 'string' || typeof vNode === 'number') {  // ✅ true
  return document.createTextNode('Item 1');  // 텍스트 노드 반환
}

→ 텍스트 노드 'Item 1' 생성 후 <li>에 append

$element.appendChild(document.createTextNode('Item 1'));
// <li id="item-1" class="list-item ">- Item 1</li>

모든 <li> 동일하게 처리

같은 방식으로 두 번째, 세 번째 <li> 생성:

// 두 번째 <li>
<li id="item-2" class="list-item ">- Item 2</li>

// 세 번째 <li>
<li id="item-3" class="list-item last-item">- Item 3</li>

최종 결과

// 1단계에서 생성한 <ul>에 3개의 <li> append됨
<ul>
  <li id="item-1" class="list-item ">- Item 1</li>
  <li id="item-2" class="list-item ">- Item 2</li>
  <li id="item-3" class="list-item last-item">- Item 3</li>
</ul>

흐름도 정리

지금까지의 흐름을 정리해보자면 아래와 같다.

normalizeVNode 결과
      ↓
createElement 호출
      ↓
type = 'ul' (문자열)<ul> DOM 엘리먼트 생성
      ↓
props 적용 (이 경우 빈 객체)
      ↓
children 반복 (3<li>)
      ↓
각 <li>마다 createElement 재귀 호출
      ↓
<li>마다 props 적용 (id, className)<li>마다 자식 처리 (텍스트 노드들)
      ↓
최종 DOM 트리 완성

이후에도 이를 활용한 renderElement, updateElement 부분이 있는데, 아직 흐름을 이해하는 중이라 적지 않았다. 추후 공부해서 더 작성해놔야겠다.


Problem

전체 흐름을 다 이해하지 못한점

  • vdom을 구성하는 createVNode, normalizeVNode, createElement 까지는 흐름을 익혔는데, 그 뒤 내용을 제대로 이해하지 못한 채 사용하게 됐다.
  • 이 부분을 다시 공부해야겠다는 생각이 든다.
  • 하드 난이도로 진행하지 않아서, 훅이나 reconcile에 대해서도 크게 고민하지 못했다. 이 부분도 알아보고 싶다.

Try

  1. AI를 활용한 부분에 대해 정확히 이해하려 노력하기

알게된 내용

1. /* @jsx createVNode / 의미

  • 컴포넌트 상단에 /** @jsx createVNode */ 가 적혀있었다. 이게 무슨 의미인가 싶어 찾아보았다.

JSX Pragma(pragmacomment)라고 불리며, 트랜스파일러(Babel 등)에게 JSX를 어떤 함수로 변환할지 알려주는 지시문

한줄 설명: JSX 문법 = createVNode 함수 호출로 자동 변환되는 것

예시:

Footer.jsx의 이 코드:

<footer className="bg-white shadow-sm sticky top-0 z-40">
  <div>내용</div>
</footer>

실제로는 이렇게 변환:

createVNode("footer", { className: "bg-white shadow-sm sticky top-0 z-40" }, [
  createVNode("div", {}, ["내용"])
])

그럼 왜 import는 하는데 직접 호출은 안 할까?

/** @jsx createVNode */
import { createVNode } from "../lib";  // ← 직접 호출 안 하는데 왜 import?

export function Footer() {
  return (
    <footer>...</footer>  // ← JSX 문법 (실제로는 createVNode로 변환됨)
  );
}

이유:

  1. 트랜스파일러가 JSX를 자동으로 변환하면서 import가 필요함
    • JSX 코드 → createVNode() 함수 호출로 변환
    • 변환된 코드를 실행하려면 createVNode 함수가 스코프에 있어야 함
  2. ESLint 경고를 피하기 위해
    • import는 했지만 직접 사용하지 않으면 unused import 경고가 뜨지만
    • pragma 주석 때문에 "이건 JSX 변환에 사용된다"고 인식

실제 동작 흐름

1️⃣ 원본 코드 (소스):
   <footer className="test"></footer>

2️⃣ Babel 트랜스파일러가 pragma 주석 읽음:
   /** @jsx createVNode */
   
3️⃣ JSX를 createVNode 함수 호출로 변환:
   createVNode("footer", { className: "test" }, [])
   
4️⃣ 변환된 코드 실행할 때 createVNode 필요:import { createVNode } from "../lib"

마치며

  1. 모각코를 했다.
  • 밤을 새웠다.
  • 우리 조 말고 다른 조 사람들도 와서 같이 했는데, 즐거웠다 ㅎㅎ
  • 학메 한 분이 고생한다며 새벽에 피자도 시켜줬다. (심지어 모각코 할 수 있는 방도 빌려줬었는데.. 이런 사람이 있다니)
  • 다들 성격이 너무 좋다.
  1. 항개팅을 했다.
  • 아는 사람들만 나왔지만, 조금 더 진지하게 대화를 나눌 수 있어 좋았다.
  • 맛있는 것도 먹어서 좋당
  • 오늘의 사진, 신도림 개맛도리 닭갈비집 😋😋😋
profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

1개의 댓글

comment-user-thumbnail
2025년 11월 23일

우와 닭갈비 집 어딘가요🤤

답글 달기