LOL로 배우는 REACT 1

김성한·2026년 9월 17일


저는 취준하고 있는 고등학교 3학년 학생ㅇ비니다.
취업 준비를 하기 위해 면접 준비를 하고 있고, 웹 지식 정리를 해야합니다
그 중에서 가장 효과적인 방법은 누군가에게 제 지식을 가르쳐주는 것이라 생각해, 방금 자바스크립트 다 배우고 이제 막 리액트를 배워야 할 롤창 소마고 1학년 학생을 상대로 가르친다고 생각하고 적어보려고 합니다
(여담으로 저는 롤 지웠습니다ㅋㅋ)

혹시나 지금 웹을 전공하려고 이걸 읽는다 -> 이 문서를 보지 말고 제발 백엔드하세요 그냥 취업 자리 없음 ㅠㅠ

리액트를 왜 쓰는가? (초급 단계)

리액트는 2013년 5월에 메타 플랫폼즈에서 개발되어 출시된 오픈 소스 JavaScript 라이브러리.

우선 일반 Vanilla JS는 속도가 빠르지만, 대형 프로젝트를 해본다면 코드가 길어지고, 동적인 것을 개발하는 데 한계가 있다고 뼈저리게 느끼실 겁니다. 그래서 만든 '라이브러리'가 '리액트'.
리액트를 쓰는 이유는 3가지로 나눌 수 있다고 생각합니다.

Component, React Hook, Virtual DOM

솔직히 이 3개를 제대로 이해한다면 당신은 AI를 통해 완벽한 서비스 구현은 가능하다 생각함.

1. Component

컴포넌트는 코드 수를 줄이고 props 값을 넣어 기본 틀을 유지한 채 값만 바꾸고 만들고 싶은 대상을 딸깍하고 집어넣을 수 있다는 게 장점임

그 전에 props라는 걸 알아야겠네요.
props는 롤 할 때 감독, 선수가 있을 거 아니에요?
감독(부모 컴포넌트)이 "너는 탑, 너는 정글" 이렇게 각 선수(자식 컴포넌트)한테 역할을 나눠주잖아요?
선수는 감독이 정해준 포지션을 받아서 그대로 경기에 나가요. 선수 마음대로 "저 오늘 미드할래요"라고 바꿀 수 없어요 — 감독이 정해준 값을 그대로 받아서 쓸 뿐암.

function ChampionCard({ nickname, line }) {
  return (
    <div>{nickname}은 {line}입니다.</div>
    // 실제 데이터 값을 표현하고 싶을 때는 중괄호 안으로 표현
  );
}

function ARAMLobby() {
  return (
    <div className="lobby">
      {/* 감독(부모)이 각 선수(자식)한테 역할(props)을 정해줌 */}
      <ChampionCard nickname="김성한" line="정글" />
    </div>
  );
}

React도 똑같아요. 부모 컴포넌트가 자식 컴포넌트한테 값(props)을 내려주면, 자식은 그 값을 그대로 받아서 화면에 쓸 뿐이지, 자식이 props 값을 직접 바꿀 수는 없어요. 마치 선수가 감독이 준 작전(포지션, 전략)을 어기지 않고 그 틀 안에서 플레이하는 것과 같아요.

자 이제 Component에 대하여 알아볼까요?

자 만약에 당신이 단일 챔프모드 롤 5인큐를 한다고 생각합시다.
그러면 챔피언의 능력, 이름 값, 속성 값 이런 건 다 똑같잖아요? 그런 거는 이제 하나의 Components 파일을 만들어서 정적 값을 만들면 됩니다. (고정 속성을 만드라는 거임)
-> 근데 여기서 다른 건 뭐겠어요? 당신의 스킨, 닉네임, 팀이겠죠. 이것들을 예시로 든 컴포넌트를 만들어 볼게요.

// 컴포넌트 코드 (야스오를 정의)
function ChampionCard({ skin, nickname, team }) {
  // 챔피언 고유 정보 - 같은 챔피언이면 누가 쓰든 항상 동일 (고정값)
  const championName = "야스오";
  const abilities = ["칼날 폭풍", "질풍참", "칼바람", "최후의 결전"];

  return (
    <div className="champion-card">
      <img src={`/skins/yasuo-${skin}.png`} alt={skin} />
      <h2>{championName}</h2>
      <p>소환사: {nickname}</p>
      <p>팀: {team}</p>
      <ul>
        {abilities.map((skill) => (
          <li key={skill}>{skill}</li>
        ))}
      </ul>
    </div>
  );
}

// 5인큐 로비 화면 - 같은 컴포넌트를 5번 재사용
function ARAMLobby() {
  return (
    <div className="lobby">
{/* 당신은 이미 야스오라는 속성을 ChampionCard() 안에 정의 해놓음. */}
{/* 그런데 이제 여기는 당신이 앞서 정해 놓은 받고 싶은 커스텀 값을 값=내용 */}
{/* 이런식으로 설명하면 됩니다. */}
      <ChampionCard skin="PROJECT" nickname="김성한" team="블루팀" />
      <ChampionCard skin="하이눈" nickname="동준" team="블루팀" />
      <ChampionCard skin="빅토리어스" nickname="민서" team="레드팀" />
      <ChampionCard skin="스타가디언" nickname="현우" team="레드팀" />
      <ChampionCard skin="기본" nickname="지훈" team="레드팀" />
    </div>
  );
}

export default ARAMLobby;

이렇게 되는 거죠. 이해가 확 가죠?

2. React Hooks

함수형 컴포넌트에서도 state(상태)나 생명주기 같은 기능을 쓸 수 있게 해주는 함수들

이렇게 설명하니까 어렵죠? 그냥 예시로 설명해 드릴게요.
React Hook에서 가장 중요하다고 생각하는 3가지 (개인의견) useState, useEffect, useRef

1. useState

useState는 가장 기본이 되는 React Hook임.

일단 기본 틀은

const [값, 바꾸는 함수] = useState(기본 값);

이렇게 생각하시면 됨.
자 그러면 const [health, setHealth] = useState(100); 이렇게 선언했으니, 나타내는 값을 쓸 때는 health를 그냥 함수처럼 쓰면 되고, 변경 시키는 값은 setHealth(함수내용) 이렇게 쓰면 됩니다.

자 근데 여기서 궁금증 const는 변하지 않는 값 상수인데 왜 바꿀 수 있을까?
-> 기본 JS문법은 값이 바뀔 때 메모리 주소까지 바꿔버리는데 React는 메모리 주소를 바꾸지 않고 안에 있는 값만 건듦.
똑같은 집 안에서 사람이 이사를 가냐 vs 집을 부수고 다른 집을 짓고 사람이 들어오냐의 차이임.

import { useState } from 'react';

function YasuoHealth({ nickname }) {
  const [health, setHealth] = useState(100);

  function onTakeDamage(damage) { // 데미지 입었을 때
    setHealth(health - damage);
  }

  return (
    <div className="yasuo-status">
      <p>소환사: {nickname}</p>
      <p>체력: {health} / 100</p>
      <button onClick={() => onTakeDamage(20)}>피격</button>
	  <button onClick={() => setHealth(health + 5)}>체력 재생</button> 
    </div>
  );
}

export default YasuoHealth;

당신이 공격을 맞으면 체력이 줄 거 아니에요?

만약 당신이 대미지를 입었어요. 일단 지금은 체력 깎이면 자동으로 반영하는 게 없으니 밑에 있는 버튼을 누릅시다. 그라믄 onTakeDamage가 선언이 되겠죠? 그러면 health-damage가 되는 거임. 만약 당신이 가독성을 포기하고 저렇게 코드 수를 줄이는 선택을 하겠다? 그러면 저렇게 써도 됨.

2. useEffect

그럴 때 useEffect를 써서 자동화를 시키면 됨.
useEffect는

useEffect(() => {
해당 조건이 발생했을 때 이벤트
}, [조건]);

의 형태로 나옴.

조건이 변경이 될 때마다 안의 구문이 계속 반복되는 거라고 이해하면 됨.

만약에 당신 체력이 30 밑으로 떨어지면 체력없는 표시를 하고싶다고 치자. 근데 이거를 아까처럼 버튼으로만 표현하면 이질감이 들겠죠?

import { useState, useEffect } from 'react';

function YasuoHealth({ nickname }) {
  const [health, setHealth] = useState(100);
  const [isDanger, setIsDanger] = useState(false);

  function onTakeDamage(damage) {
    setHealth(health - damage);
  }

  useEffect(() => {
    if (health <= 30) {
      setIsDanger(true);
    } else {
      setIsDanger(false);
    }
  }, [health]);

  return (
    <div className="yasuo-status">
      <p>소환사: {nickname}</p>
      <p>체력: {health} / 100</p>
      <p>{isDanger ? "⚠️ 위험 상태" : "정상"}</p>
      <button onClick={() => onTakeDamage(20)}>피격</button>
      <button onClick={() => setHealth(health + 5)}>체력 재생</button>
    </div>
  );
}

export default YasuoHealth;

그래서 다음과 같이 useEffect를 사용하면 됨.

지금 보면 버튼을 누르면 계속 피격이 되면서 피가 줄어들겠죠? 그러면 useEffect는 계속 반응함.
-> 왜냐하면 health의 값이 바뀔 때 반응하도록 만들어졌기 때문임. 계속 피가 닳다 보면 useEffect안의 if (health<=30){}문도 걸리게 될 거임.
그러면 setIsDanger(true);가 성립이 되니까 저기 있는 정상이라는 글자가 "⚠️ 위험 상태"로 변경될 거임.

3. useRef

useRef는 좀 특이한데, 값이 바뀌어도 React가 이 변화를 감지를 못함. 그래서 useState처럼 화면(UI)이 자동으로 다시 그려지지 않아요.

const attackCount = useRef(0); // 얘는 변경 값을 안쓰고 .current를 사용해서 값 변경

function onAttack() {
  attackCount.current += 1; // 리렌더링 발생 안 함
}

useRef는 .current를 통해서 값을 증가.
값은 분명히 바뀌었는데, 화면에 그 값을 그냥 {count}처럼 표시해뒀다면 그 부분은 갱신이 안 되고 예전 값 그대로 보여요.

그래서 useRef는 "화면에 실시간으로 안 보여줘도 되는 값"을 다룰 때 주로 써요. 예를 들어 사용자한테는 안 보여주지만 서버로 보낼 통계용 카운트, 렌더링 횟수 추적, 또는 DOM 요소를 직접 잡아서(inputRef.current.focus()처럼) 조작할 때 사용해요.

그래서 useRef는 게임에 전혀 필요없는 산수; 예를 들어 야스오가 게임에서 얼마나 맞았는지, 딜을 어느정도 넣었는지 게임 내에서 전혀 표현할 필요 없는 내용들을 선언함.

근데 그러면 여기서 질문이 생길 거임.

아니 그러면 useState쓰면 되지 왜 굳이 useRef를 씀?

이라고 질문을 한다면 당신은 고수.
const [health, setHealth] = useState(100)에서 health는 매번 렌더링될 때마다 새로 선언되는 지역 상수임.
setHealth를 호출하면, React가 "다음 렌더링에서는 health를 이 새 값으로 다시 선언해줘"라고 예약해두는 거고, 그래서 매 렌더링마다 health는 새로 생기는 const라 "값을 바꾸는 게" 아니라 "새로운 값을 가진 새 const로 교체되는 것"임.
더 간단하게 설명하자면 React는 집은 나둔 채 이사를 가는 거지만, JS는 집을 부수고 새 집 짓고 사는 거임.

그리고 useRef는 특이하게 DOM요소에 접근할 때도 쓰임.

import { useRef } from 'react';

function YasuoHealth() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current.focus(); // 실제 <input> DOM 요소에 직접 접근해서 포커스 줌
  }

  return (
    <div>
      <input ref={inputRef} placeholder="닉네임 입력" />
      <button onClick={focusInput}>입력창 포커스</button>
    </div>
  );
}

만약에 채팅창에 focus효과를 주고싶다. 하면 DOM 안에 있는 input을 건드려서 .focus를 적용시켜 줘야하는데
React에서 이것을 해주는 hook이 바로 useRef임.
그래서 이것은 너무 광범위해서 여러분들이 직접 필요한 button, input들의 속성 값들을 찾아 쓰는 효과적이에용

3. Virtual DOM

이걸 알기 전에 일단 HTML은 DOM요소로 이루어져 있다는 걸 알고 계셔야합니다.

이거 얘기하면 좀 길어지니까 DOM이라는 건 여러분들이 검색해 보세요. 대충 이렇게 생겼습니다.
저는 그냥 Virtual DOM 설명만 해드릴게요.

Virtual: 가상의

우선 Virtual DOM은 지금 당신의 DOM을 복사해서 만든 가벼운 DOM임.

당신은 게임을 할 때 상대의 킬, 아이템을 지속적으로 보기 위해서 탭키를 눌러서 게임 스코어보드를 볼 거 잖아요?
여기서 스코어보드의 킬 값이 추가되거나 템이 업데이트 되거나 할 때 생각을 해볼게요.


// 유저 데이터
let players = [
  { name: "김성한", kills: 5, deaths: 2, assists: 8 },
  { name: "동준", kills: 3, deaths: 4, assists: 6 },
  { name: "민서", kills: 7, deaths: 1, assists: 3 },
  // ... 10명까지 있다고 가정
];

function renderScoreboard() {
  const html = players.map(p => `
    <div class="player-row">
      <span>${p.name}</span>
      <span>${p.kills}/${p.deaths}/${p.assists}</span>
    </div>
  `).join('');

  document.getElementById('scoreboard').innerHTML = html;
}

function onKill(playerIndex) { // 킬을 먹으면 재렌더링
  players[playerIndex].kills++;
  renderScoreboard(); // 1명 킬 먹었을 뿐인데, 10명 전체를 다 다시 그림
}

자 만약에 탑에서 솔킬이 일어났어요. 베인이 티모를 땄어요. 그러면 onKill이 성립 되겠죠? 위 코드 대로라면 renderScoreboard(); 가 실행돼서 렌더링이 되는 거예요.
근데 이 때 HTML은 DOM 요소들을 새로 만듭니다. 트리에 나무통?이랑 나뭇가지만 남고 잎 다 떨어졌다가 다시 핀다고 생각하면 됨. 하지만 DOM이라는 거 자체는 굉장히 브라우저에서 비용이 많이드는(힘든) 작업입니다. 근데 바뀔 때마다 계속 만들어낸다? 이거는 엄청 버겁겠죠. 그러니 성능도 떨어집니다. 하지만 리액트는 다릅니다.

import { useState } from 'react';

function Scoreboard() {
// 유저 데이터
  const [players, setPlayers] = useState([
    { name: "김성한", kills: 5, deaths: 2, assists: 8 },
    { name: "동준", kills: 3, deaths: 4, assists: 6 },
    { name: "민서", kills: 7, deaths: 1, assists: 3 },
  ]);

  function onKill(index) {
    const updated = [...players];
    updated[index].kills++;
    setPlayers(updated);
  }

  return (
    <div id="scoreboard">
      {players.map((p, i) => (
        <div key={p.name} className="player-row">
          <span>{p.name}</span>
          <span>{p.kills}/{p.deaths}/{p.assists}</span>
          <button onClick={() => onKill(i)}>킬</button>
        </div>
      ))}
    </div>
  );
}

export default Scoreboard;

리액트는 다르게 접근합니다. 값이 바뀌면, 먼저 메모리 안에서 바뀌기 전 Virtual DOM과 바뀐 후 Virtual DOM, 이렇게 가벼운 복사본 두 개를 만들어요. 그다음 이 둘을 비교해서 "정확히 어디가 달라졌는지"를 찾아요. 그리고 찾아낸 그 부분만 진짜 DOM에 추가하거나 삭제하거나 수정해줍니다.

이제 결론을 내려봅시다. 변경이 일어난다면 처음부터 다 그릴래요? 아니면 비교해서 바뀐 부분만 바꿀래요?

이건 지나가던 아이언 5한테 물어도 후자를 선택합니다. 이게 바로 Virtual DOM의 장점입니다.

아 그런데 Virtual DOM을 사용한다고 무작정 빠른 건 아닙니다.
가상돔은 상태 변경 시 트리를 새로 만들고 비교하는 diff 비용이 추가로 들기 때문에, 정확한 지점만 직접 조작하는 방식보다 빠르지는 않습니다. 대신 전체 재렌더링보다 효율적이고 선언적으로 UI를 짤 수 있게 해주는 게 장점이며, 불필요한 리렌더링은 memo 같은 최적화로 직접 관리해야 합니다.

읽어주셔서 감사합니다. 다음에 반응 좋으면 추가 라이브러리에 대해서 만들어볼게요

0개의 댓글