React Learn - 리액트의 등장 배경과 컴포넌트의 기본적 개념

ChoiYongHyeun·2024년 2월 17일
0

리액트

목록 보기
2/31
post-thumbnail

리액트의 Learn React 5강 챕터 중 첫 번째 챕터를 공부하고 추상화 된 개념을
다시 적어보는 공부 기록장입니다
Describing the UI

React의 마크업 언어와 프로그래밍 언어의 경계 허물기

예전의 웹처럼 정적인 형태의 페이지들이 많았던 시기에는

HTML , CSS , Javscript 파일 3가지를 각각 독립적인 위치에서 작성하고 각 문서 별

역할의 분리가 확실했다.

HTML 문서는 마크업 언어를 작성하고, CSSHTML 태그들의 스타일링을 하고 JS 는 작성된 마크업 언어들을 이용해 logical 한 일들을 하였다.

하지만 점점 웹이 인터렉티브하게 바뀌면서 JS 의 역할 비중이 늘어나기 시작했으며

JS 가 마크업의 역할을 하던 HTML 의 역할까지 하게 되었다.

이미 존재하는 DOM 트리에서 이벤트 핸들링만 하는 것이 아니라, 직접적으로 DOM 에 노드를 추가하거나 삭제, 수정 하는 로직이 많이 생겼다.

이렇게 각 역할군의 경계가 모호해짐에 따라 문서들을 분할해서 관리하는 것이 비효율적으로 느껴졌다.

그로 인해 JS 에서 마크업 언어까지 사용 할 수 있게 도와주는 JSX 를 활용해 그 경계를 무너 뜨렸다.

리액트의 Component Oriented 철학은 DOM 의 노드를 이룰 수 있는 독립적인 단위의 컴포넌트들을 정의하고

정의된 컴포넌트들을 레고블럭 쌓듯 쌓아 DOM 트리를 생성한다.

// 컴포넌트를 이용해 생성한 DOM 트리 예시 
<PageLayout>
  <NavigationHeader>
    <SearchBar />
    <Link to="/docs">Docs</Link>
  </NavigationHeader>
  <Sidebar />
  <PageContent>
    <TableOfContents />
    <DocumentationText />
  </PageContent>
</PageLayout>

JSX 를 반환하는 컴포넌트들의 조합으로 다음처럼 구성 할 수 있다.


MPA,SSR => SPA , CSR

Multi Page Application , Sever side rendering

기존 정적인 웹의 경우엔 Multi Page ApplicationMPA 가 주를 이뤘다.

MPA 는 클라이언트가 요구할 페이지들을 서버 단에서 HTML ,CSS, JS 형태로 모두 미리 만들어둔 채로 서버단에서 페이지를 렌더링 하고

Sever Side Rendering (SSR) : 서버단에서 페이지를 렌더링하는 행위

클라이언트 요구에 따라 페이지 형태로 모두 전달한다.

MPA , SSR 을 이용하면 페이지의 마크업 언어가 HTML 형태로 작성되어있기에 SEO 에서는 더 좋을 수 있지만

페이지를 이동 할 때 마다 문서를 받아와야 하기에 페이지 이동 사이에 생기는 Blink 현상이 존재할뿐더러

페이지들은 독립적인 마크업 언어 , 스타일링 언어 , 프로그래밍 언어들로 이뤄져있어

상태 관리와 확장성이 낮다는 단점이 존재한다.

Single Page Application , Client side rendering

Single Page Application(SPA) 은 말그대로 서버는 클라이언트에게 하나의 페이지만을 제공하고

클라이언트의 요청에 따라 페이지를 구성하는 데이터를 요청, 클라이언트 단에서 렌더링을 진행하는 Clinet side rendering 를 이용한다.

브라우저의 렌더링 엔진이 발전함에 따라 클라이언트 단에서 렌더링 하는 속도가 서버 단에서 렌더링 하여 보내주는 속도보다 빨라졌을 뿐더러

CSR 을 이용하면 페이지간 이동에서 오는 Blink 현상도 없어 더 높은 UX 를 제공 할 수 있다.

다만 예시에서 MPA 는 받아오는 네트워크 요청이 많고 SPA 는 요청이 적었던 것 처럼 보이는데
이는 MPA , SPA 의 차이가 아니라 예시로 들은 MPA 의 예시인 뉴욕타임즈에서는 여러 임지들을 요청받아오면서 네트워크 활동량이 많아 보인다.

MPA 여서 네트워크 요청량이 많은 것은 아니다
하지만 페이지 이동 시 나타나는 Blink 현상은 MPA 여서 그런 것이 맞다.

서버 단에서는 렌더링을 하기 위한 자료구조 (주로 JSON) 를 받아 클라이언트 단에서 JS 를 실행하여 페이지를 렌더링 하는데

이런 렌더링을 위해서 설명한 컴포넌트를 이용 할 수 있다.


컴포넌트 지향 개발의 이점

CSR 에서 봤듯이 JS 를 이용해 페이지를 렌더링 함으로서

Markup langaugePromgraming language 의 경계가 허물어졌음을 확실히 알았다.

그럼 JS 를 이용해 페이지를 렌더링 할 때

스크립트 별로 직접적으로 DOM 을 조작하는 것이 아니라, 컴포넌트들을 이용해 DOM 을 구성하는 것의 장점이 뭘까 ?

페이지 구성의 특징으로 보는 컴포넌트의 필요성

페이지들은 모두 유사한 기능을 하는 영역들의 반복으로 이뤄진다.

비컴포넌트 지향 개발에선 반복되는 동일한 영역들을 구성하기 위해 코드를 반복적으로 작성했다면

각 형태를 구성하는 컴포넌트를 만들어두고 주어지는 데이터에 따라 다른 내용들을 채워넣는 것이

컴포넌트 지향 개발이다.

만일 페이지의 레이아웃을 변경하고 싶거나, 해당 영역의 구성을 변경 , 새로운 영역을 추가 하고 싶다면

컴포넌트 지향 개발에선 전체 페이지 구성에서 컴포넌트의 위치만 변경하거나

컴포넌트 자체의 로직만 변경해주면 된다.

혹은 새로운 컴포넌트를 추가해주거나

하지만 비컴포넌트 지향 개발에서는 조금만 수정하려해도 코드 전체를 손봐야 하는 비효율적인 일이 일어난다.

이처럼 컴포넌트 지향개발은 관리용이성이 높고 확장성이 높다.


JSX 야 컴포넌트 만들기를 도와줘 ~!

리액트에서 컴포넌트를 만드는 방법은 다음과 같다 .

const element = React.createElement('div', { className: 'container' },
  React.createElement('h1', null, 'Welcome to My Website'),
  React.createElement('ul', null,
    React.createElement('li', null, 'Item 1'),
    React.createElement('li', null, 'Item 2'),
    React.createElement('li', null, 'Item 3')
  ),
  React.createElement('p', null, 'This is a paragraph.'),
  React.createElement('button', { onClick: () => alert('Button clicked!') }, 'Click me')
);

리액트에서 제공하는 메소드를 이용해 컴포넌트를 구성해봤다.

마치 document 내부에 존재하는 메소드와 유사해보이지만 차이점은

document.createElement 는 실제 DOM 에 생성을 한다면 React.createElement 는 리액트의 virtual DOM 에 생성한다는 차이가 있다.

이건 나중에 다루도록 하자

매번 컴포넌트를 만들 때 우린 이렇게 해야 할까 ?

아니다 JSX 라는 syntanx extension 을 이용하면 HTML 마크업 언어를 마치 JS 에서 바로 작성하듯 사용 할 수 있다.

function MyComponent() {
  return (
    <div className="container">
      <h1>Welcome to My Website</h1>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
      <p>This is a paragraph.</p>
      <button onClick={() => alert('Button clicked!')}>Click me</button>
    </div>
  );
}

const element = MyComponent();

JSXReact 만이 가지고 있는 유일한 기술이 아닌 , JS 에서 마크업 언어와 유사한 코드를 직접 작성 할 수 있게 해주는 Syntaxx extension 이다.

JSX 로 작성된 언어는 컴파일 중 일반 JS 함수 호출로 변환되어 노드를 생성한다.

<div className="container"> => React.createElement('h1', null, 'Welcome to My Website'),

그러니 JSX 는 마크업 문자열처럼 보이지만, 사실은 일종의 객체임을 우리는 알아야 한다.

JSX 익스텐션을 이용해 우리는 노드 객체를 마크업 문자열의 형태로 손쉽게 만들 수 있다.

이런 JSX 의 조합으로 컴포넌트를 구성하도록 하자

JSX 야 고마워 ~!!@!@!@@!

JSX 를 이용하기 위한 규칙

그럼 JSX 를 이용 할 거니까 JSX 를 이용할 규칙을 생각해보자

1. Return a single root element

return(
  <div>
  	<h1>Hedy Lamarr's Todos</h1>
    <img 
      src="https://i.imgur.com/yXOvdOSs.jpg" 
      alt="Hedy Lamarr" 
      class="photo"
    >
  	<ul>
    ...
  	</ul>
</div>
)

항상 반환 할 때는 하나의 부모 노드에 종속되어 있는 한 컴포넌트 덩어리를 반환해야 한다.

만약 컴포넌트 덩어리를 생성하기 위해 사용되는 div 태그를 사용하고 싶지 않다면

<>
  <h1>Hedy Lamarr's Todos</h1>
  <img 
    src="https://i.imgur.com/yXOvdOSs.jpg" 
    alt="Hedy Lamarr" 
    class="photo"
  >
  <ul>
    ...
  </ul>
</>

다음과 같은 <>... </> 를 이용해도 된다. 이런 것은 Fragment 라고 한다.

Fragment 는 노드에 추가 되지 않는다.

이는 document.createDocumentFragment 로도 구현이 되어 있다.
JSX 에만 있는 유일한 형태가 아니라는 말

그럼 의문이 든다.

왜 ? 그냥 마크업 언어를 반환해서 붙이는거니까 꼭 한 덩어리일 필요가 있나 ?

JSX 는 마크업 언어가 아니라 객체다. 마크업 언어처럼 생긴 객체

반환되는 객체를 DOM 에 추가하든 Virtual DOM 에 추가하든 결국엔 어떤 한 객체를 추가해야 한다.

만약 부모 태그가 2개인 요소를 반환하도록 한다면 그건 객체 하나가 아닌, 두 개의 객체를 반환하는 것과 같다.

2. Close all the tags

항상 태그는 시작 태그와 종료 태그가 존재해야 한다.

그것이 시작 태그와 종료 태그가 존재하지 않는 <img> 태그라고 하더라도 하나만 사용할 때

<img /> 와 같이 종료지점을 명시해줘야 한다.

왜 ?

JSX 는 문자열을 구문 분석하여 객체를 생성하기에

원활한 구문 분석을 위해 시작 지점과 종료 지점을 명시해줘야 한다.

3. camelCase all most of the things!

JSXJS 기반이니 마크업 언어처럼 <div class = ...> 이렇게 사용하면 안된다.

태그 안에 존재하는 attribute 는 자바스크립트 객체의 key 들로 들어간다.

자바스크립트의 문법을 따르도록 하자

class -> className , background-color -> backgroundColor 와 같이 말이다.


JSX 에서 Javascript Logic 사용하기

JSX 는 마크업 언어 형태로 생긴 자바스크립트 객체들이다.

그럼 JSX 의 마크업 언어에서 Javascript 의 로직이 사용가능하단 것이다.

뭐 반복문을 쓴다거나 조건문을 쓴다거나 변수를 쓴다거나 하는 행위 등등 ..

JSX 에서는 Javsciprt logic 을 사용하기 위해서 brace({}) 를 사용해주자

export default function Avatar() {
  const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
  const description = 'Gregorio Y. Zara';
  return (
    <img // 프로퍼티 값으로 {} 이용하여 변수 넣기 
      className="avatar"
      src={avatar}
      alt={description}
    />
  );
}

export default function TodoList() {
  const name = 'Gregorio Y. Zara';
  return (
    <h1>{name}'s To Do List</h1> // 문자열 내에서 {} 로 변수 넣기 
  );
}

const today = new Date();

function formatDate(date) {
  return new Intl.DateTimeFormat(
    'en-US',
    { weekday: 'long' }
  ).format(date);
}

export default function TodoList() {
  return (
    <h1>To Do List for {formatDate(today)}</h1> // {} 로 함수 반환값 넣기 
  );
}

{} 가 존재하지 않는 부분은 모두 문자열로 취급된다.

만약 {} 내부에서 {} 를 또 쓰고 싶다면

예를 들어 객체 리터럴

또 쓰면 된다.

export default function TodoList() {
  return (
    <ul style={{ // {} 로 감싸진 객체 리터럴을 {} 로 한번 더 감싸줘 js logic 임을 명시
      backgroundColor: 'black',
      color: 'pink'
    }}>
      <li>Improve the videophone</li>
      <li>Prepare aeronautics lectures</li>
      <li>Work on the alcohol-fuelled engine</li>
    </ul>
  );
}

Props 맛보기

컴포넌트들이 뭘 하는데 ? 그리고 어떻게 사용하는데 ? 에 대한 질문들은 위에서 맛보기로 알아봤다.

props 를 이해하기 위해선 컴포넌트의 존재 이유를 한 번 더 상기해 볼 필요가 있다.

컴포넌트들은 하나의 틀이다. 들어오는 데이터에 따라서 다른 형태의 레이아웃들이 반환되는

그러면 들어오는 데이터들을 받을 argument 가 필요할 것임을 직감 할 수 있다.

물론 데이터를 받지 않고 항상 같은 결과값을 뱉는 컴포넌트도 존재 할 수 있다.

컴포넌트가 받는 argument 들을 props 라고 부른다.

function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

다음처럼 Avatar 컴포넌트는 person , size 라는 인수를 받아 다른 이미지 노드들을 반환하는 컴포넌트이다.

어째서 객체 디스트럭처링을 썼는지는 추후 설명한다.

export default function Profile() {
  return (
    <div>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi', 
          imageId: 'YfeOqp2'
        }}
      />
      <Avatar
        size={80}
        person={{
          name: 'Aklilu Lemma', 
          imageId: 'OKS67lh'
        }}
      />
      <Avatar
        size={50}
        person={{ 
          name: 'Lin Lanying',
          imageId: '1bX5QH6'
        }}
      />
    </div>
  );
}

Profile 이란 컴포넌트는 하위 요소로 Avatar 컴포넌트들을 호출하는데

각기 다른 props 들을 전달함으로서 다른 모습의 컴포넌트 결과값들을 반환 받을 수 있었다.

 <Avatar // <> 내부에서 argument 들을 key , value 형태로 전달 
        size={50}
        person={{ 
          name: 'Lin Lanying',
          imageId: '1bX5QH6'
        }}
      />

// 여기서 호출된 Avatar 는 다음과 같은 인수들을 전달 받아 
function Avatar({ person <- {name : 'Lin ...' , imageId : '1bx..'}, size <- 50 }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name} // 여기에 쏙쏙들이 들어갑니다 
      width={size}
      height={size}
    />
  );
}

중요한 포인트는 하위 컴포넌트는 항상 상위 컴포넌트에게서 argument 를 전달 받는다는 점이다.

그러니 하위 컴포넌트의 결과값에 영향을 미치는 것은 항상 상위 컴포넌트이다.

그럼 여기서 의문 !

??? : 너는 지금 자꾸 argument 이야기 하는데 도대체 props 는 언제 나와 ?

function Avatar({ person, size }) { // 왜 디스트럭 처링을 하는거지?
  									// person , size 그냥 이렇게 받으면 안돼 ? 
  ..
}

나도 그렇게 생각했다.

하지만 React 에서 컴포넌트는 항상 법칙이 존재한다.

그건 바로 ~~ Component 의 인수는 항상 props 객체이다.

props 객체라고 해서 뭐 대단한건 아니고, 그냥 key , value 형태로 이뤄진 일반적인 객체와 같다.

 <Avatar 
        size={50} // size 는 props 의 property , 50 은 props.size 의 값 
        person={{ // props.person = {name : ... , imageId : ...}
          name: 'Lin Lanying', // 처럼 하여 argument 로 전달됨 
          imageId: '1bX5QH6'
        }}
      />

React 는 컴포넌트들이 받는 인수의 형태를 props 라는 객체 하나로 강제함으로서

통일성을 줄 수 있도록 하였다.

function Avatar(props){
  	// 사실 로직은 이런거였음 ㅋㅋ 
	const size = props.size;
  	const person = props.person;
}

그래서 바로 다음처럼 디스트럭처링을 하는 것보다 애체에 컴포넌트를 선언 할 때 부터

디스트럭처링하여 인수를 받도록 하는 것이다.

상위 컴포넌트의 props 를 모두 사용 할 게 아니니까

몇 가지 추가로 짚고 나갈 이야기들

  1. Component 선언 할 때 props 의 기본값을 설정 할 수 있다.
function Avatar({ person, size = 50 }){
... // props 에서 size 프로퍼티가 없거나 props.size = undefined 일 때는 기본값이 사용됨
}
  1. Spread 문법 사용 댕가능
function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} /> 
      // 굳이 key  = value 형태로 전달 안하고 스프레드 문법과 디스트럭처링해서
      //전달 해줘도 됩니다.  						
    </div>
  );
}
  1. Parsing children
<Card> // Card 컴포넌트의 하위 컴포넌트로
  <Avatar /> // Avatar 컴포넌트를 넣어주고 싶으면 ? 
// 아니 애초에 넣으면 Card 컴포넌트에서 하위 컴포넌트는 어떻게 처리되지 ? 
</Card>

다음처럼 하위 컴포넌트 자체를 넣게 되면 Card 컴포넌트의 props 는 어떻게 인식하냐면

<Card> 
  <Avatar /> // props.children = <Avatar /> 으로 들어갑니다  
</Card>
function Card({ children }) {
  return (
    <div className="card">
      {children} // Card 컴포넌트의 하위 컴포넌트들은 모두 여기에 넣어줘 ! 
    </div>
  );
}

props.children 프로퍼티 값으로 하위 컴포넌트들을 넣어 상위 컴포넌트의 인수로 전달해준다.


Conditional rendering

이건 삼항 연산자나 조건에 따른 props 변경에 대한 이야기 ..

Conditional Rendering 참고 요망


배열을 React 에서 이용하여 렌더링 하는 법

우리가 JSX 를 이용하면서 까지 JS 에서 DOM 조작을 하고 싶은 이유는

JS 의 효율적인 로직들을 이용하기 위함이다.

반복문, 조건문부터 하여 배열의 고차함수, 다양한 자료구조를 이용한 렌더링 등등 ..

그럼 배열을 이용해서 렌더링 한다고 해보자

<ul>
  <li>Creola Katherine Johnson: mathematician</li>
  <li>Mario José Molina-Pasquel Henríquez: chemist</li>
  <li>Mohammad Abdus Salam: physicist</li>
  <li>Percy Lavon Julian: chemist</li>
  <li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>

다음처럼 반복적인 li 태그를 넣어주고 싶다면 다음처럼 하면 된다.

const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
]; // 내용을 자료구조에 담아주고 
const listItems = people.map(person => <li>{person}</li>); // JSX 들이 담긴 배열로 만들어주고

return <ul>{listItems}</ul>; // 담아서 반환 뻄                         

JSX 에서는 {} 내부에 존재하는 JSX 객체들을 모두 스프레드 시켜 렌더링 한다.
굳이 스프레드 시켜버리거나 .join('') 과 같은 것을 사용해줄 필요 없다.

상당히 직관적이다.

??? : 어 ~ 반복되는 일은 배열 고차함수를 이용해서 하면 돼 ~ 리액트 쉽네 ~

하지만 다음처럼 시행하면 경고창이나 오류창이 뜨며 다음과 같이 말한다.

Warning: Each child in a list should have a unique “key” prop.

배열로 생성되는 노드들은 항상 유니크한 key prop 을 가져야 한다고 한다.

생성되는 JSX elements 들에게 항상 key prop 을 건내주어야 한다는 것이다.

<li>{person}</li> // <- 이건 안돼
<li key = '어떤 property'>{person}</li> // <- 이렇게 key 값에 props 를 건내줘야해

왜요 ?

Key propVirtual DOM 의 연관성

이 부분에 대한 내용을 열심히 검색해봐도
key propindex 로 사용하면 안되는 이유에 대한 글만 나와서 좀 더 내가 주관적으로 이해한 바를 적어보았다.

아직 공식문서에서 해당 내용이 나오지는 않았지만 ReactVirtual DOM 을 이용하는 방식에 대해

먼저 이해 할 필요가 있다.

Actual DOM 을 조작하는 것은 상당히 무겁기 때문에 React 는 노드가 변경 될 때 마다 Actual DOM 을 조작하는 것이 아닌

변경 전 후의 Virtual DOM 을 만들어 두고 Virtual DOM 중 변경점들을 모아

한 번에 일괄적으로 Actual DOM 에 적용하는 Batch DOM Manipulate 방식을 이용한다.

Actual DOM 이 일어날 때 변경 전의 Virtual DOM 도 업데이트 된다.

이렇게 변경 전 후의 Virtual DOM 을 비교하는 과정을 Diffing , 변경 전 후의 내용을 Actual DOM 에 적용하는 과정을 reconciliation 이라고 한다.

Diffing 과정에서 변경 전 후의 Virtual DOM 노드들을 재귀적으로 순회하며 만나는 노드들의

노드의 타입 , 어트리뷰트 (props) , 자식 노드 , key 를 비교하여

해당 내용들이 맞지 않는 경우 변경이 된 것으로 간주하여 해당 노드 이하의 모든 내용을 모두 다시 렌더링한다.

여기서 내가 궁금한 점은 도대체 key prop 은 왜 필요할까 ? 였다.

그럼 key prop 이 존재할 때와 존재하지 않을 때 발생하는 일을 생각해보자

key props 가 존재하지 않는 평행 세계의 리액트

export default function App() {
  const koreaStew = ['김치찌개', '된장찌개', '부대찌개'];

  return (
    <ul>
      {koreaStew.map((stew) => (
        <li>{stew}</li>
      ))}
    </ul>
  );
}

만약 다음과 같이 한국에서 먹을 수 있는 찌개 리스트를 표현하는 컴포넌트가 존재한다 생각해보자

이 때 대통령이 된장찌개 금지령을 내려서 된장찌개를 앞으로 못먹게 하고 , 새로 개발한 개구리찌개만 먹으라고 했다고 해보자

export default function App() {
  const koreaStew = ['김치찌개', '부대찌개', '개구리찌개'];
  // 된장찌개 삭제, 개구리찌개 추가

  return (
    <ul>
      {koreaStew.map((stew) => (
        <li>{stew}</li>
      ))}
    </ul>
  );
}

이 때 리액트는 어떻게 virtual DOM 간의 차이를 확인할까 ?

직관적으로 생각하면

??? : ㅋㅋ 아 ~ 노드끼리 순회하면서 각자 순서에 맞게 태그 확인, 어트리뷰트 확인 , 자식 태그들 확인 하겠지 ㅋㅋ

이렇게 생각이 드는데

이는 우리가 배열의 index 라는 개념에 너무 익숙해져서 순서에 맞게 확인한다는 것이 당연하게 느껴지기 때문이다.

사실 key prop 이 없는 평행 세계에서는 순서라는 개념은 존재하지 않는다.

수정 전 후의 virtual DOMbefore dom , after dom 처럼 부르겠다.
0. after dombefore domul 태그에 도착함
1. after dom 에서 김치찌개 노드에 도착하면 before dom 의 모든 노드를 순회하며 존재 확인
2. after dom 에서 부대찌개 노드에 도착하면 before dom 의 모든 노드를 순회하며 존재 확인
3. after dom 에서 개구리찌개 노드에 도착하면 before dom 의 모든 노드를 순회하며 존재 확인, 존재하지 않으니 Actual DOM 에서 업데이트

어떤 노드의 존재 여부를 확인하기 위해 해당 형제 노드의 모든 노드를 확인한다는 것이

너무나도 비효율적이란 것이 느껴진다.

n 개의 노드를 가진 virtual DOM 에서 어떤 노드를 비교 할 때 마다 n 개의 모든 노드를 순회해야 하기 때문에 O(n2)O(n^2) 이라는 스튜핏한 시간 복잡도를 가지게 된다.

key prop 이 존재하는 우리 세계의 React

그럼 형제 노드에서 어떤 노드를 식별 할 수 있는 유일한 식별자인 key property 를 만들어주자

이 때 key 들은 형제노드에서 유니크한 하나의 값으로 해당 노드를 식별 할 수 있는 유일한 식별자 역할을 해야 한다.

virtaul DOM 을 순회하며 차이점을 확인 할 때에는 이 key prop 을 이용해서

해당 형제 노드에 key prop 이 같은 노드가 존재하는지를 통해 노드의 존재 유무를 쉽게 확인 할 수 있으며 존재 한다면 태그 , 자식 노드 등이 모두 이전과 동일한지 확인하기만 하면 되어 이전보다 훨씬 효율적이다.

리액트에서는 형제 노드 별로 모든 노드가 유니크한 key prop 을 가지도록 강제하며

생성 할 때 key prop 을 지정해주지 않을 경우에는 암묵적으로 key prop 을 해당 배열에서의 index 로 선언한다.

      {koreaStew.map((stew) => (
        <li>{stew}</li>
      ))} // 다음은 사실 

      {koreaStew.map((stew , index) => (
        <li key = {index}>{stew}</li> // 처럼 생긴겁니다
      ))}

그로 인해

[김치찌개, 된장찌개, 부대찌개] -> ['김치찌개' , 부대찌개 , 개구리찌개] 로 변한 경우는

key 가 1인 된장찌개 -> 부대찌개 로 변하였으니 업데이트 하고
key 가 2인 부대찌개 => 개구리찌개로 변하였으니 업데이트 한다.

import React, { useState } from 'react';

export default function App() {
  let [koreaStew, setStew] = useState(['김치찌개', '된장찌개', '부대찌개']);
  const handleClick = () => {
    const newStew = koreaStew.filter((stew) => stew !== '된장찌개'); // 된장찌개 삭제
    newStew.push('개구리찌개'); // 개구리찌개 추가
    setStew(newStew);
  };

  return (
    <>
      <ul>
        {koreaStew.map((stew) => (
          <li>{stew}</li>
        ))}
      </ul>
      <button onClick={handleClick}>개구리찌개를 먹으세요</button>
    </>
  );
}

실제로 렌더링 되는 요소들을 보면 key 값이 1, 2 인 요소들이 새롭게 렌더링 되는 모습을 볼 수 있다.

정리

Virtual DOM 에서 노드들이 변화 유무를 효율적으로 알기 위해서
각 노드를 식별할 수 있는 유일한 식별자인 key props 가 필수적으로 필요하다.

이로 인해 React 에서는 노드들에게 key props 를 의무적으로 배치하도록 하며
배열을 이용한 노드들에게 key props 를 설정해주지 않았을 때에는 배열 내 원소들의 인덱스를
이용한다.

ㅋㅋ 그럼 key 값으론 배열의 인덱스를 사용하면 되나요 ?

사용하지 않는 것이 좋다.

그 이유로 key 값은 해당 노드를 식별하는 유일한 식별자이면서 immutable 해야 한다.

immutable 해야 하는 이유는 위 예시로 충분히 설명 가능하다.

위 예시에서 부대찌개라는 노드는 변경 과정에서 수정된 것이 없기 때문에 새롭게 렌더링 될 필요가 없다.

하지만 새롭게 렌더링 된 이유는 key 값을 index 로 설정하였기 때문에

배열의 모습이 변경됨에 따라 부대찌개의 인덱스 값이 변경, 그로 인해 key 값도 변경된다.

이는 지금처럼 노드의 구조가 단순하다면 큰 문제가 되지 않지만

부대찌개 노드에 수 없이 많은 자식 노드들이 존재한다면 이 모든 노드를 다시 렌더링 하는 것은

비용적으로 비효율적이다.

이처럼 key 값을 index 로 사용하는 것은 mutablekey 값을 사용함에 따라 생길 수 있는 다양한 문제를 초래한다.

React index as key | Don't use index as a key
해당 영상을 통해 문제를 확인해보자
리액트에서 노드들의 상태는 key props 를 통해 저장된다.

그럼 어떻게 해야 하는데 ?

자료구조 내에서 key props 에 지정해줄 수 있는 어떤 값을 설정해주자

export default function App() {
  let [koreaStew, setStew] = useState([ // koreaStew 에서 객체들에게 id 를 갖도록 지정
    { id: 1, content: '김치찌개' },
    { id: 2, content: '된장찌개' },
    { id: 3, content: '부대찌개' },
  ]);
  const handleClick = () => {
    const newStew = koreaStew.filter(({ content }) => content !== '된장찌개');
    newStew.push({ id: 4, content: '개구리찌개' }); // 새로운 값을 추가 할 때도 id 추가
    setStew(newStew);
  };
  return (
    <>
      <ul>
        {koreaStew.map(({ id, content }) => (
          <li key={id}>{content}</li>
        ))}
      </ul>
      <button onClick={handleClick}>개구리찌개를 먹으세요</button>
    </>
  );
}

이번에는 자료구조에서 id 값을 지정해주도록 설정을 해주고 렌더링을 다르게 해보았다.

index 가 아닌 특정 id 를 이용해보니

된장찌개가 제거되고 부대찌개가 자료구조에서의 위치가 변경되더라도

부대찌개는 본인만의 id 값을 가지고, 해당 idkey props 로 사용되니

위치가 변경되더라도 재렌더링 되지 않는 모습을 볼 수 있다.

이 때 key props가 갖는 다양한 조건들

  • key props 들은 형제 노드에서만 유일하면 된다
  • key props 를 유니크하게 하겠다고 Math.random() 같은거 쓰지 말자

    이러면 렌더링 될 때 마다 모든 노드의 key props 값이 바뀌고 모두 재렌더링 될 것이다.


컴포넌트는 퓨어하게 관리해야 한다.

컴포넌트가 퓨어하다는 것은 어떤 것일까 ?

1. 컴포넌트는 주어진 입력값이 같다면 항상 같은 출력값을 반환해야 한다.

컴포넌트는 노드 객체를 뱉어내는 하나의 틀 같은 역할을 한다.

들어오는 props 에 따라서 다른 내용들을 갖는 노드 객체를 반환하지만

들어오는 props 가 같다면 항상 같은 노드 객체를 반환해야 한다.

2. 컴포넌트는 상위 환경의 변수를 변경하면 안된다.

자바스크립트에서 변수의 스코프는 하위 지역 환경으로 향하기 때문에

컴포넌트는 컴포넌트 외부에서 정의된 변수에 접근 하는 것이 가능하다.

하지만 컴포넌트는 상위 환경의 변수를 오로지 읽기 전용으로만 접근해야 하며

직접적으로 수정하면 안된다.

만약 상위 변수에 직접적으로 접근하여 값을 변경 할 수 있다면 1번 성질을 만족하지 못한다.

let guest = 0;

function Cup() {
  guest = guest + 1; // 상위 변수의 값을 수정함 
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup /> // guest = 2 
      <Cup /> // guest = 4
      <Cup /> // guest = 6
    </>
  );
}

들어오는 값이 동일함에도 불구하고 다른 출력값을 뱉고 있는 모습을 볼 수 있다.

왜 1,2,3 이 아닌지에 대해서는 나중에 state 를 공부 할 떄 이야기 해보자

컴포넌트에게 퓨어함을 강제하는 이유

리액트는 클라이언트가 발생시킨 이벤트로 인해 발생한 변경된 부분과

실제 전달하는 페이지의 모습이 동일하기를 바란다.

그런데 만약 페이지 렌더링 과정에서 의도치 않게 상태가 변경되거나 예상치 못하게 다른 부분이 변경된다면

정확한 페이지를 제공 할 수 없다.

이를 방지하기 위해 리액트는 렌더링부분과 상태 변경 부분을 엄격하게 분리시켜두기 위해

컴포넌트를 퓨어하게 관리하고자 한다.

이러한 강제성은 익숙해지기만 한다면 강제성 덕분에 컴포넌트를 더욱 자유롭게 사용 할 수 있게 해줄 것ㅇ디ㅏ.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글