본문 바로가기

코드잇 스터디

[React] 리엑트에 대하여

사용 이유

- 새 페이지 들어가면 부드럽게 전환

- 컴포넌트로 코드 재사용성 늘리기

- html로 코딩하면 어떻게 바꿔야하는지 일일이 지시해야하고, 수정시에 바꿔야하는 곳이 많다

    -> 유지보수가 어려움

- JS와 HTML을 한큐에 처리 가능
    -> 무엇을 보여줄지만 정의할 수 있음

- 데이터가 변경되면 화면은 자동으로 업데이트

 

React 플젝 시작

Vite로 만든다. 환경설정을 자동으로 빠르게 세팅해준다. 

현재 프로젝트에서 바로 만들었으면 좋겠으면 npm create vite@latest .

(온점넣어줘야함)을 해줘야한다. 

이렇게 세팅하고, 프로젝트로 이동한 후

npm i로 필요한 패키지를 설치한다. 

그러고 npm run dev해서 개발 서버 실행하면 된다. 

실행하면 다음과 같은 기본 화면이 나온다

이 세팅을 boilerplate라고 부른다. 

프로젝트 시작을 위한, 준비 셋업 템플릿이다. 

 

Vite가 좋은 이유 - HMR (Hot Module Replacement)

변경한 파일만 새로 다운로드 받는 방식 -> 그래서 훨씬 더 빠름

 

JSX

- Javascript안에 HTML이 들어가 있는 문법 (Javascript XML)

- 내부적으로 데이터가 어떻게 변환되는지 몰라도, 데이터 변경 시 알아서 화면이 전환됨 

 

<규칙>

1. 반드시 하나의 태그로 감싸고 리턴해야함

    적어도 Fragment로 감싸줘야한다. 

    (모두 div로 감싸주면 HTML 구조가 너무 복잡해진다.)

function TableRow() {
  return (
    <>
      <td>이름</td>
      <td>나이</td>
    </>
  );
}
//<React.Fragment>도 있긴 한데 길어서 잘 안 사용한다.

React.Fragment는 map을 사용해서 key를 빈태그에 작성해줘야할때 필요하다. 

 

2. HTML 속성은 camelCase로 작성해야함

 

3. 모든 태그는 닫아야 함

 

JSX에서 JS 사용하기

- JSX안에 JS 표현식을 넣고 싶을때 {}를 사용한다. 

- {}안에 함수 실행해서 결과값을 넣을수도 있으니,

    연산 방식이 복잡하면 함수로 빼는게 좋다.

    ⭐JSX는 뭘 보여줄지에 집중하고, 어떻게 계산할지는 함수로 빼라

- 삼항 연산자는 좋으나,

    중첩 삼항 연산잔는 이해하기 어렵기 시작함으로 함수로 따로 분리하여

- 리스트를 화면 보여줄때 가장 자주 사용하는 패턴이 map과 filter이다. 

    ⭐key에다가 index를 넣지 말아라, 나중에 CRUD할때 문제가 생긴다. -> 안티패턴

        고유한 ID를 key에 넣어줘야한다. 

- 템플릿 리터럴으로 어떤 상태에 어떤 class를 가져라를 표현한다.

- JSX내에 IIFE사용하는것도 읽기 어려워지므로 지양해야한다. -> 안티패턴

 

Component

화면을 구성하는 독립적인 부품

JSX를 반환하는 함수이다. 

function Welcome() {
  return <h1>환영합니다!</h1>;
}

PascalCase로 시작해야함 (WritePostPage)

 

Props

정의

부모컴포넌트가 자식 컴포넌트에 보내는 property (데이터)

다만, 자식은 props를 절대 수정할 수 없다. 

값을 가공하고 싶으면 새 변수에 할당해주는 것이 좋다. 

// 자식 컴포넌트
function Greeting(props) {
  return <h1>안녕하세요, {props.name}님!</h1>;
}

// 부모 컴포넌트
function App() {
  return (
    <div>
      <Greeting name="홍길동" />
      <Greeting name="김철수" />
      <Greeting name="이영희" />
    </div>
  );
}

 

근데 이걸 구조분해하면 훨씬 편하게 사용할 수 있다. 

- boolean 타입은 true인 경우에 생략이 가능하다. 

function UserCard({ name, age, job }) {   // ← 이 부분!
  return (
    <div>
      <h2>{name}</h2>
      <p>{age}세</p>
      <p>{job}</p>
    </div>
  );
}
<Counter count={0} />                           {/* 숫자 */}
<Modal isOpen={true} />                         {/* 불린 */}
<Button disabled={true} />                         // 정석
<Button disabled />                                // 축약 (true와 동일!)
<TagList tags={['React', 'JS', 'CSS']} />      {/* 배열 */}
<Profile user={{ name: '홍길동', age: 25 }} />   {/* 객체 */}
<Form onSubmit={handleSubmit} />                 {/* 함수 */}

 

Props의 기본값 정해주기

function Button({ text = '버튼', color = 'blue' }) {
  return <button style={{ color }}>{text}</button>;
}
// 사용
<Button />                              {/* "버튼", 파란색 */}
<Button text="클릭" />                  {/* "클릭", 파란색 */}
<Button text="클릭" color="red" />      {/* "클릭", 빨간색 */}

 

Spread 연산자로 한번에 전달하기

객체를 통째로 넘기고 싶을때 ...을 쓰면 편하다.

function App() {
  const user = {
    name: '홍길동',
    age: 25,
    job: '개발자'
  };

  // 😐 일일이 전달
  return <UserCard name={user.name} age={user.age} job={user.job} />;

  // ✨ Spread 연산자 (훨씬 간결!)
  return <UserCard {...user} />;
}

 

Children

부모의 컨포넌트 태그 사이에 들어가는 JSX 내용이다. 

밑의 예시에서 <h2>와 <p>가 children이다. 

<Card>
  <h2>제목</h2>    
  <p>설명</p>
</Card>

 

주의해야하는 안티패턴

children이 빈 경우를 꼭 챙겨야한다. 

 

State

컴포넌트가 기억하는 값이다. 얘가 변하면 화면이 자동 업데이트한다. 

React Memory 내에 정의되기 때문에 리렌더링 되어도 값을 유지한다. (메모리가 함수 바깥에 위치되어 있음)

배경

⭐상태가 변경되면, 리렌더링이 일어난다 (해당 컴포넌트 전체가 다시 불려와짐)

    이때, 변수들은 리렌더링 시 모두 새 변수(이전과 다른 주소를 가진)를 정의해서 사용하기 때문에,
    일반 변수들을 수정하는 식으로 코드를 짜면 딱 한번만 변하고 그 이후는 변하지 않는다

onClick을 누르면 count state는 의도대로 잘 바뀐다.
반면 변수로 정의한 count2는 11로 바뀌고, 리렌더링 되어서 완전히 새 count2가 정의되어
앞으로 눌러도 영원히 11이 된다. . 

 

문법

배열 구조분해 할당은 구조분해된 변수들 이름을 마음대로 정할 수 있음

 

두가지 state 업데이트 방식

function handleClick() {
  // 방법 1: 직접 값
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
  // → 1만 증가 😱

  // 방법 2: 함수 전달
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  // → 3 증가 ✅
}

이렇게 작동하는 이유가, React가 배치처리(Batching)을 하기 때문이다.

화살표 함수는 각자 다른 참조값을 가지기 때문에

서로 다른 값을 참조한다. 
그래서 모아서 한번에 처리해도 이전 상태를 잘 받아온다. 

 

참조형 state

객체와 배열을 state로 쓸때의 함정을 피하고, 불변성을 지키며 안전하게 업데이트해야한다. 

React는 state가 변했는지 확인할때 ===(참조) 비교를 한다. 

그래서 참조주소가 바뀌어지만 화면 리렌더링을 한다. 

⭐리액트는 상태변경 요청을 받았을 때 현재 상태와 다음 상태가 다를때만 리렌더링을 실시한다. 

const obj = { name: '홍길동' };
obj.name = '김철수';   // 값만 바꿈
setUser(obj);          // 같은 주소의 객체

// React: "어? 주소가 똑같네? 안 바뀐 거네!" 😴
// → 화면 업데이트 안 함!

 

그래서 새로운 객체/배열을 setState안에 새로 할당해줘야한다. 

수정액으로 지우지 말고, 복사본을 만들어서 새로 써야한다

(불변성 - Immutability, 원본을 건들지 말고 카피해서 새로 만들어라)

// ✅ 새 객체 생성 (React가 알아챔!)
setUser({ ...user, name: '김철수' });

⭐ 기존것 다 펼치고, 바꿀 것만 덮어쓰기하면 된다.

//추가하는 방식: spread operator사용
function addTodo(newTodo) {
  setTodos([...todos, newTodo]);   // 새 배열! ✨
}
//삭제하는 방식: filter은 새 배열 만들기에 작동함
function deleteTodo(id) {
  setTodos(todos.filter(todo => todo.id !== id));
  //              ↑↑↑↑↑↑
  //     "id가 일치하지 않는 것만 남겨줘"
}
//수정하는 방식: map도 새 배열 만들기에 작동함
function toggleTodo(id) {
  setTodos(todos.map(todo =>
    todo.id === id
      ? { ...todo, done: !todo.done }   // 해당 항목만 바꾸기
      : todo                             // 나머진 그대로
  ));
}

(sort, revert, pop, etc는 모두 원본을 바꾸기에 사용하지 않는다)

 

주의점


React가 돌아가는 방식

React가 렌더링하는 방식

학습 목표: React의 엔진룸을 들여다본다. 어떻게 그렇게 빠르게 화면을 업데이트하는지, 그 마법의 정체를 밝힌다.

(1) 리액트에서 말하는 "렌더링"이 정확히 뭐예요?

놀랍게도 렌더링 = 컴포넌트 함수를 실행하는 것 이에요.

function Counter() {
  const [count, setCount] = useState(0);
  return <button>{count}</button>;
}

렌더링된다 = React가 Counter() 를 호출한다는 뜻.

🤯 충격의 진실

렌더링 ≠ 화면이 실제로 바뀌는 것!

"렌더링"
    ↓
함수 실행 → JSX 반환 → 가상 DOM 만들기
    ↓
"그 다음에" 화면 업데이트 (별개의 단계!)

많은 사람이 헷갈리는 부분이에요. 기억하세요:

렌더링은 "계산"이고, 화면 업데이트는 "반영"이에요.

(2) 두 단계로 나뉘어요

React의 렌더링은 2단계로 이루어져요.

1️⃣ Render Phase (계산 단계) — "설계도 그리기"

  • 컴포넌트 함수 호출
  • JSX 반환
  • 가상 DOM 생성
  • 이전 가상 DOM과 비교

2️⃣ Commit Phase (적용 단계) — "실제 건설"

  • 실제 DOM에 변경사항 반영
  • 화면에 보여지는 순간

🏗️ 비유: 건축 공사와 똑같아요.

  • Render = 건축가가 설계도 그리기 (머릿속으로만)
  • Commit = 인부들이 실제로 건물 짓기 (눈에 보이는 결과)

설계도를 여러 번 고쳐도 실제 공사는 최종 버전 한 번만 하죠? React도 똑같아요!

(3) 언제 렌더링될까?

컴포넌트가 다시 렌더링되는 4가지 경우예요.

1. 처음 화면에 나타날 때 (최초 렌더링)

<App />   // 앱이 시작되면 한 번 렌더링

2. State가 바뀔 때

setCount(count + 1);   // → 리렌더링!

3. Props가 바뀔 때

<UserCard name={newName} />   // name이 바뀌면 → 리렌더링!

4. 부모가 렌더링될 때 ⚠️

function Parent() {
  return (
    <div>
      <Child />   {/* 부모가 렌더링되면 자식도 같이! */}
    </div>
  );
}

🚨 중요: 부모가 렌더링되면 자식은 Props가 안 바뀌어도 리렌더링돼요!

이게 성능 문제의 주요 원인이기도 해요. (나중에 최적화로 해결!)

(4) Virtual DOM — 똑똑한 비서

🤔 왜 Virtual DOM이 필요해?

DOM 직접 조작은 무거워요. 한 번 건드릴 때마다 브라우저가 전체 화면을 다시 계산해야 해요.

// 10번 DOM 조작 = 10번 계산 😩
document.getElementById('item1').textContent = '...';
document.getElementById('item2').textContent = '...';
// ...

🧠 Virtual DOM의 아이디어

"머릿속으로 먼저 해보고, 최종 결과만 실제로 적용하자!"

바뀐 부분만 실제 DOM에 반영하는 것이다. 

1. State 변경!
    ↓
2. 🧠 새 Virtual DOM 만들기 (메모리에서만)
    ↓
3. 🔍 이전 Virtual DOM과 비교 (Diffing)
    ↓
4. 📝 "아, 이것만 바뀌었네!" 최소 변경 목록 작성
    ↓
5. 🎨 실제 DOM에 그 부분만 반영 (Reconciliation)

🧑‍🍳 비유: 요리 시뮬레이션

❌ Virtual DOM 없이

냉장고 열기 → 확인 → 닫기
냉장고 열기 → 확인 → 닫기
냉장고 열기 → 확인 → 닫기
...

✅ Virtual DOM 방식

🧠 머릿속으로 전부 시뮬레이션
    ↓
📝 "필요한 재료 10개" 리스트 작성
    ↓
🥶 냉장고 한 번만 열어서 다 꺼내기!

훨씬 효율적이죠?

(5) 배치 업데이트 (Batching) — 영리한 모아 처리

여러 setter를 한 번에 처리하는 React의 최적화 기능이에요.

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  function handleClick() {
    setCount(count + 1);     // 렌더링 예약
    setName('업데이트됨');    // 렌더링 예약
    setCount(count + 2);     // 렌더링 예약
    // → 이벤트가 끝난 뒤 "한 번만" 렌더링!
  }

  console.log('렌더링!');   // 한 번만 출력됨
}

🤔 왜 이렇게 해?

만약 배치 안 하면? 3번 렌더링해서 화면이 번쩍번쩍 거려요. 😵

배치 처리로 마지막 결과만 한 번 렌더링 → 부드럽고 빠름! ✨

💡 React 18부터는 setTimeout, Promise 안에서도 자동 배치가 돼요. (예전엔 안 됐음!)


JSX로 자유롭게 클래스 추가하기

동적 클래스 조합

템플릿 리터럴 JS로 클래스를 조합할 수 있다 .

function Button({ variant }) {
  return (
    <button className={`btn btn-${variant}`}>
      클릭
    </button>
  );
}
// 사용
<Button variant="primary" />   {/* className="btn btn-primary" */}
<Button variant="danger" />    {/* className="btn btn-danger" */}

 

조건부 클래스 3가지 패턴

1) 삼항 연산자 (단일 조건)

function TodoItem({ todo }) {
  return (
    <div className={`todo-item ${todo.done ? 'done' : ''}`}>
      {todo.text}
    </div>
  );
}

 

2) && 연산자 (조건부 추가) ❌

function Button({ disabled }) {
  return (
    <button className={`btn ${disabled && 'btn-disabled'}`}>
      클릭
    </button>
  );
}
  • disabled가 true면 → "btn btn-disabled"
  • false면 → "btn false" ⚠️ (어? false가 들어가네?!)

🚨 주의! false && 'xxx'는 false를 반환해요. 그럼 className에 "false" 라는 문자가 들어가요!

대신 이렇게 쓰세요: disabled ? 'btn-disabled' : '' (삼항) 또는 다음 패턴 사용!

 

3) 배열 + filter + join (여러 조건)

- 여러 class들을 배열로 넣는다. 

function Button({ variant, size, disabled }) {
  const classNames = [
    'btn',
    `btn-${variant}`,
    `btn-${size}`,
    disabled && 'btn-disabled'
  ].filter(Boolean).join(' ');
  //  ↑↑↑↑↑↑↑↑↑↑↑↑
  //  false/null/undefined 제거 후 공백으로 합치기

  return <button className={classNames}>클릭</button>;
}

 

CSS Modules - 파일별 스코프 격리 (강력추천)

이게 없으면 다른 css 파일이라도, 안에 같은 이름의 클래스를 다루면

둘이 충돌이 나게 된다. 

CSS Modules를 사용하면 css 파일이 자신만의 스코프를 가져서 문제가 생기지 않는다.