React

[1편] 리액트 코드 기초 : 한 줄씩 완벽 이해하기


💡 이 문서의 목표: 코드의 모든 부분을 마치 선생님이 옆에서 설명하듯이 자세히 풀어서 설명합니다.


📌 시작하기 전에 꼭 알아야 할 것들

JavaScript 기초 용어 정리

// 1. 변수 선언 - 데이터를 담는 상자 만들기
const name = "철수";     // const = 변하지 않는 상자
let age = 15;            // let = 변할 수 있는 상자  
var old = "옛날방식";     // var = 요즘 안 쓰는 방식

// 2. 함수 - 일을 시키는 명령어 모음
function 인사하기() {    // function = "함수를 만들겠다"
  console.log("안녕");   // console.log = "화면에 출력해줘"
}

// 3. 화살표 함수 - 함수를 짧게 쓰는 방법
const 인사 = () => {     // () => {} 이 모양이 화살표처럼 생겼죠?
  console.log("안녕");
}

1️⃣ 첫 번째 컴포넌트 만들기 - 완전 상세 버전

📝 Header.js 파일 - 한 줄씩 뜯어보기

// 🔍 1번 줄: 함수 선언
function Header() {
  • function = “나는 함수를 만들 거야”라는 신호
  • Header = 함수 이름 (⚠️ 반드시 대문자로 시작!)
    • 왜 대문자? → 리액트가 “아, 이건 컴포넌트구나!”라고 알아차리려면
    • 소문자 header로 쓰면? → HTML 태그인 줄 알고 혼란스러워해요
  • () = 매개변수를 받는 곳 (지금은 비어있음)
  • { = 함수의 시작을 알리는 중괄호
// 🔍 2번 줄: return 시작
  return (
  • return = “이제 결과를 돌려줄게”
  • ( = 여러 줄의 JSX를 쓸 때 사용하는 소괄호
    • 왜 소괄호? → 여러 줄을 묶어주는 역할
// 🔍 3-5번 줄: JSX 내용
    <header>
      <h1>나의 멋진 웹사이트</h1>
    </header>
  • <header> = HTML의 header 태그 (의미: 페이지 상단 영역)
  • <h1> = HTML의 제목 태그 (가장 큰 제목)
  • 나의 멋진 웹사이트 = 실제로 화면에 보이는 텍스트
  • </h1> = h1 태그 닫기
  • </header> = header 태그 닫기
// 🔍 6-7번 줄: 함수 종료
  );
}
  • ) = return 문의 소괄호 닫기
  • ; = 문장 끝 (선택사항이지만 쓰는 게 좋아요)
  • } = 함수 끝을 알리는 중괄호
// 🔍 8번 줄: 내보내기
export default Header;
  • export = “다른 파일에서 쓸 수 있게 내보낼게”
  • default = “이 파일의 대표 내용이야”
  • Header = 내보낼 컴포넌트 이름
  • ; = 문장 끝

🎯 전체 코드 동작 과정

// Header.js 전체 코드와 동작 설명
function Header() {           // 1️⃣ Header라는 함수(컴포넌트) 정의 시작
  return (                   // 2️⃣ 이 컴포넌트가 화면에 그릴 내용 시작
    <header>                 // 3️⃣ HTML header 태그 열기
      <h1>제목입니다</h1>     // 4️⃣ 제목 텍스트
    </header>                // 5️⃣ HTML header 태그 닫기
  );                         // 6️⃣ return 문 끝
}                            // 7️⃣ 함수 정의 끝

export default Header;       // 8️⃣ 다른 파일에서 import 할 수 있게 내보내기

// 이 코드가 실행되면?
// 1. Header() 함수가 메모리에 저장됨
// 2. 다른 파일에서 import Header from "./Header" 로 불러올 수 있음
// 3. <Header /> 로 사용하면 return 안의 내용이 화면에 나타남

2️⃣ 컴포넌트 사용하기 (App.js) - 완전 상세 버전

// App.js - 한 줄씩 설명
import "./App.css";                    // 1️⃣ CSS 파일 불러오기
import Header from "./component/Header"; // 2️⃣ Header 컴포넌트 불러오기

function App() {                       // 3️⃣ App 컴포넌트 정의 시작
  return (                             // 4️⃣ 화면에 그릴 내용 시작
    <div className="App">              // 5️⃣ div 태그 with CSS 클래스
      <Header />                       // 6️⃣ Header 컴포넌트 사용!
    </div>                            // 7️⃣ div 태그 닫기
  );                                   // 8️⃣ return 문 끝
}                                      // 9️⃣ 함수 끝

export default App;                    // 🔟 App 컴포넌트 내보내기

🔍 각 줄 상세 설명

// 1️⃣번 줄 분석
import "./App.css";
  • import = “가져와줘”라는 명령
  • "./App.css" = 가져올 파일 경로
    • ./ = 현재 폴더에서
    • App.css = 이 이름의 CSS 파일을
  • 왜 중괄호 {}가 없나요? → CSS는 그냥 스타일만 적용하면 되니까
// 2️⃣번 줄 분석
import Header from "./component/Header";
  • import Header = Header라는 이름으로 가져와줘
  • from = ~로부터
  • "./component/Header" = 파일 경로
    • ./ = 현재 폴더에서 시작
    • component/ = component 폴더 안의
    • Header = Header.js 파일 (.js는 생략 가능)
// 6️⃣번 줄 특별 설명
<Header />
  • <Header = Header 컴포넌트 사용 시작
  • /> = 자체 닫기 태그 (self-closing)
  • 이게 실행되면? → Header.js의 return 내용이 여기에 들어감!

📊 import/export 관계도

graph LR
    A[Header.js] -->|export default Header| B[내보내기]
    B -->|import Header from| C[App.js]
    C -->|<Header />| D[화면에 표시]
    
    style A fill:#ffe4b5
    style B fill:#e6f3ff
    style C fill:#f0fff0
    style D fill:#ffcccc


3️⃣ JSX에서 JavaScript 사용하기 - 중괄호의 비밀

기본 예제: 변수 출력하기

function Body() {
  // 🔍 변수 선언부 - 데이터 준비
  const number = 10;           // 숫자 타입 변수
  const name = "철수";          // 문자열 타입 변수
  const isStudent = true;      // 불리언(참/거짓) 타입 변수
  
  // 🔍 return 부분 - 화면에 그리기
  return (
    <div>
      {/* 이 부분이 주석입니다. 화면에 안 보여요 */}
      
      <h2>{number}</h2>
      {/* 
        위 코드 설명:
        1. <h2> = HTML 제목 태그 시작
        2. { = JavaScript 모드 시작! (중요!)
        3. number = 변수 이름 (위에서 10을 넣었죠?)
        4. } = JavaScript 모드 끝!
        5. </h2> = HTML 제목 태그 끝
        
        결과: <h2>10</h2> 이 되어서 화면에 "10"이 보임
      */}
      
      <h2>{name}님 안녕하세요!</h2>
      {/*
        분석:
        6. {name} = "철수"로 치환됨
        7. 결과: <h2>철수님 안녕하세요!</h2>
        8. 화면: "철수님 안녕하세요!"
      */}
      
      <h2>{number + 5}</h2>
      {/*
        계산식도 가능!
        9. {number + 5} = {10 + 5} = {15}
        10. 결과: <h2>15</h2>
        11. 화면: "15"
      */}
      
      <h2>{isStudent}</h2>
      {/*
        ⚠️ 주의! true/false는 화면에 안 나타남!
        결과: <h2></h2> (빈 태그)
        화면: 아무것도 안 보임
      */}
    </div>
  );
}

🎯 중괄호 사용 규칙 정리

// ✅ 중괄호 안에 넣을 수 있는 것들
<div>
  {100}                     // 숫자 ✅
  {"안녕"}                  // 문자열 ✅
  {myVariable}              // 변수 ✅
  {10 + 20}                 // 계산식 ✅
  {myFunction()}            // 함수 호출 ✅
  {isTrue ? "맞아" : "틀려"} // 삼항 연산자 ✅
</div>

// ❌ 중괄호 안에 넣으면 안 되는 것들
<div>
  {true}                    // boolean - 화면에 안 보임 ❌
  {false}                   // boolean - 화면에 안 보임 ❌
  {{name: "철수"}}          // 객체 - 에러 발생! ❌
  {[1, 2, 3]}              // 배열 - 조심해서 써야 함 ⚠️
</div>

4️⃣ Props 완전 정복 - 데이터 전달의 모든 것

부모 컴포넌트 (App.js)

function App() {
  // 🔍 1단계: 전달할 데이터 준비
  const userName = "김철수";     // 변수에 데이터 저장
  const userAge = 15;           // 숫자도 가능
  const userHobby = "게임";      // 문자열
  
  return (
    <div>
      {/* 🔍 2단계: 자식 컴포넌트에 데이터 전달 */}
      <UserCard 
        name={userName}        // name이라는 이름으로 userName 변수값 전달
        age={userAge}         // age라는 이름으로 userAge 변수값 전달
        hobby={userHobby}     // hobby라는 이름으로 userHobby 변수값 전달
        school="중앙중학교"    // 직접 문자열 전달도 가능!
      />
      {/*
        위 코드가 하는 일:
        1. UserCard 컴포넌트를 부름
        2. 4개의 데이터를 전달:
           - name="김철수"
           - age=15
           - hobby="게임"
           - school="중앙중학교"
        3. 이 데이터들이 props라는 객체로 묶여서 전달됨
           props = {
             name: "김철수",
             age: 15,
             hobby: "게임",
             school: "중앙중학교"
           }
      */}
    </div>
  );
}

자식 컴포넌트 (UserCard.js) - 방법 1

// 🔍 방법 1: props 객체로 통째로 받기
function UserCard(props) {
  // props는 이런 모양입니다:
  // {
  //   name: "김철수",
  //   age: 15,
  //   hobby: "게임",
  //   school: "중앙중학교"
  // }
  
  console.log(props);         // 전체 props 객체 확인
  console.log(props.name);    // "김철수" 출력
  console.log(props.age);     // 15 출력
  
  return (
    <div>
      <h2>이름: {props.name}</h2>
      {/* 
        props.name 분석:
        1. props = 전달받은 객체
        2. . = 객체의 속성에 접근
        3. name = 속성 이름
        4. 결과: "김철수"
      */}
      
      <p>나이: {props.age}살</p>
      {/* props.age = 15 */}
      
      <p>취미: {props.hobby}</p>
      {/* props.hobby = "게임" */}
      
      <p>학교: {props.school}</p>
      {/* props.school = "중앙중학교" */}
    </div>
  );
}

자식 컴포넌트 (UserCard.js) - 방법 2

// 🔍 방법 2: 구조 분해 할당으로 받기 (더 편해요!)
function UserCard({ name, age, hobby, school }) {
  // 이 한 줄이 아래와 같은 의미:
  // const name = props.name;
  // const age = props.age;
  // const hobby = props.hobby;
  // const school = props.school;
  
  // 이제 props. 없이 바로 사용 가능!
  return (
    <div>
      <h2>이름: {name}</h2>    {/* props.name 대신 name만! */}
      <p>나이: {age}살</p>      {/* props.age 대신 age만! */}
      <p>취미: {hobby}</p>      {/* 훨씬 깔끔하죠? */}
      <p>학교: {school}</p>
    </div>
  );
}

📊 Props 데이터 흐름도

sequenceDiagram
    participant App as App (부모)
    participant Props as Props 객체
    participant UserCard as UserCard (자식)
    
    App->>Props: name="김철수" 담기
    App->>Props: age=15 담기
    App->>Props: hobby="게임" 담기
    App->>Props: school="중앙중학교" 담기
    Props->>UserCard: 전체 객체 전달
    UserCard->>UserCard: props.name으로 사용
    UserCard->>UserCard: props.age로 사용

5️⃣ 이벤트 처리 완벽 이해

onClick 이벤트 - 클릭 처리하기

function ButtonExample() {
  // 🔍 1단계: 이벤트 핸들러 함수 정의
  const handleClick = () => {
    console.log("버튼이 클릭되었습니다!");
    alert("안녕하세요!");
  };
  // 위 코드 설명:
  // - const handleClick = 변수에 함수를 저장
  // - () => { } = 화살표 함수
  // - 함수 안의 코드는 버튼 클릭 시 실행됨
  
  return (
    <div>
      {/* 🔍 2단계: 버튼에 이벤트 연결 */}
      <button onClick={handleClick}>
        클릭하세요
      </button>
      {/*
        onClick={handleClick} 분석:
        1. onClick = "클릭했을 때"라는 이벤트
        2. = 연결
        3. {handleClick} = 실행할 함수 이름
        4. ⚠️ 주의: handleClick() 아님! 괄호 없음!
        
        왜 괄호가 없나요?
        - handleClick = 함수 자체를 전달 (나중에 실행해줘)
        - handleClick() = 함수를 지금 바로 실행 (잘못된 방법!)
      */}
      
      {/* ❌ 잘못된 예시 */}
      <button onClick={handleClick()}>
        잘못된 예시 (페이지 로드하자마자 실행됨!)
      </button>
      
      {/* ✅ 매개변수가 필요한 경우 */}
      <button onClick={() => console.log("직접 작성")}>
        직접 함수 작성
      </button>
      {/*
        () => console.log("직접 작성") 분석:
        1. () => 새로운 화살표 함수 생성
        2. 이 함수는 클릭할 때만 실행됨
        3. 그때 console.log가 실행됨
      */}
      
      {/* ✅ 매개변수 전달하기 */}
      <button onClick={() => alert("안녕")}>
        인사하기
      </button>
      <button onClick={() => alert("잘가")}>
        작별인사
      </button>
    </div>
  );
}

onChange 이벤트 - 입력 처리하기

function InputExample() {
  // 🔍 이벤트 객체(e) 완벽 이해
  const handleChange = (e) => {
    // e = 이벤트 객체 (이벤트에 대한 모든 정보)
    console.log("전체 이벤트 객체:", e);
    console.log("이벤트 종류:", e.type);           // "change"
    console.log("이벤트 발생 요소:", e.target);    // <input> 요소
    console.log("입력된 값:", e.target.value);     // 사용자가 입력한 텍스트
    console.log("요소의 이름:", e.target.name);    // name 속성값
    console.log("요소의 타입:", e.target.type);    // "text", "number" 등
  };
  
  return (
    <input 
      type="text"
      name="username"
      onChange={handleChange}
      placeholder="입력하면서 콘솔을 확인하세요"
    />
  );
  
  /*
    동작 과정:
    1. 사용자가 키보드로 뭔가 입력
    2. 입력이 바뀔 때마다 onChange 이벤트 발생
    3. handleChange 함수 자동 실행
    4. 이벤트 객체(e)가 자동으로 전달됨
    5. e.target.value로 현재 입력값 확인 가능
  */
}


6️⃣ useState 한 글자도 놓치지 않고 이해하기

기본 구조 분석

// 🔍 import 문 분석
import { useState } from "react";
// import = 가져오기
// { useState } = react 라이브러리에서 useState 함수만 가져오기
// from "react" = react 패키지로부터

function Counter() {
  // 🔍 useState 사용법 완벽 분석
  const [count, setCount] = useState(0);
  /*
    이 한 줄을 풀어서 설명하면:
    
    1. useState(0) 실행
       - useState 함수에 초기값 0을 전달
       - useState는 배열을 반환: [현재값, 변경함수]
       
    2. 배열 구조 분해 할당
       - [count, setCount] = [0, 함수]
       - count = 0 (현재 상태값)
       - setCount = 값을 바꾸는 함수
       
    3. const로 선언
       - count와 setCount는 변하지 않는 변수
       - 하지만 count의 "값"은 setCount로 변경 가능
  */
  
  // 🔍 일반 변수와 비교
  let normalVariable = 0;  // 일반 변수
  
  // 🔍 값 변경 함수
  const increase = () => {
    // State 변경 (✅ 화면 업데이트됨)
    setCount(count + 1);
    // 위 코드가 실행되면:
    // 1. count + 1 계산 (0 + 1 = 1)
    // 2. setCount(1) 실행
    // 3. React가 count 값을 1로 변경
    // 4. 컴포넌트 다시 렌더링
    // 5. 화면의 숫자가 1로 바뀜
    
    // 일반 변수 변경 (❌ 화면 업데이트 안됨)
    normalVariable = normalVariable + 1;
    // 값은 바뀌지만 React가 모름 → 화면 그대로
    
    console.log("State:", count + 1);        // 다음 State 값
    console.log("일반 변수:", normalVariable); // 변경된 값 (하지만 화면은 그대로)
  };
  
  return (
    <div>
      <h2>State 값: {count}</h2>
      <h2>일반 변수: {normalVariable}</h2>
      <button onClick={increase}>+1 증가</button>
      
      {/* 
        버튼 클릭 시 동작:
        1. increase 함수 실행
        2. setCount(count + 1) 실행
        3. React가 변경 감지
        4. Counter 컴포넌트 재실행 (리렌더링)
        5. 새로운 count 값으로 화면 다시 그리기
      */}
    </div>
  );
}

useState의 동작 원리 시각화

graph TB
    A["useState(0) 호출"] --> B[초기 상태 생성]
    B --> C["count = 0"]
    B --> D["setCount 함수 생성"]
    
    E[버튼 클릭] --> F["setCount(1) 호출"]
    F --> G[React가 변경 감지]
    G --> H[컴포넌트 리렌더링]
    H --> I["count = 1로 화면 업데이트"]
    
    style A fill:#ffd700
    style F fill:#ff6347
    style H fill:#32cd32
    style I fill:#87ceeb

7️⃣ 입력 폼과 State 연결하기

한 개의 입력창 관리

function SingleInput() {
  // 🔍 입력값을 저장할 State
  const [text, setText] = useState("");
  // "" = 빈 문자열로 시작 (입력창이 비어있음)
  
  // 🔍 입력 이벤트 처리 함수
  const handleChange = (e) => {
    // e.target = input 요소
    // e.target.value = 현재 입력창에 있는 텍스트
    const inputValue = e.target.value;
    console.log("입력된 값:", inputValue);
    
    // State 업데이트
    setText(inputValue);
    // 또는 바로: setText(e.target.value);
  };
  
  return (
    <div>
      <input 
        type="text"
        value={text}           // ⚠️ 중요! State와 연결
        onChange={handleChange} // 입력할 때마다 실행
        placeholder="입력해보세요"
      />
      {/*
        value={text}가 중요한 이유:
        1. input의 값을 React가 관리 (제어 컴포넌트)
        2. text State가 바뀌면 input도 자동 업데이트
        3. 프로그램으로 입력값을 바꿀 수 있음
      */}
      
      <p>입력한 내용: {text}</p>
      <p>글자 수: {text.length}</p>
      
      {/* State를 활용한 조건부 렌더링 */}
      {text.length > 10 && (
        <p style={{color: 'red'}}>10글자를 초과했습니다!</p>
      )}
      
      <button onClick={() => setText("")}>
        초기화
      </button>
      {/* setText("")로 빈 문자열 설정 → input도 비워짐 */}
    </div>
  );
}

여러 입력창 관리 (객체 State)

function MultipleInputs() {
  // 🔍 여러 입력값을 하나의 객체로 관리
  const [formData, setFormData] = useState({
    name: "",      // 이름 초기값
    age: "",       // 나이 초기값
    email: ""      // 이메일 초기값
  });
  // formData = { name: "", age: "", email: "" }
  
  // 🔍 범용 입력 처리 함수 (모든 input에서 사용)
  const handleChange = (e) => {
    // e.target.name = input의 name 속성값 ("name", "age", "email")
    // e.target.value = 입력된 값
    
    const fieldName = e.target.name;   // 어떤 input인지
    const fieldValue = e.target.value; // 입력된 값
    
    console.log(`${fieldName} 필드에 ${fieldValue} 입력됨`);
    
    // 🔍 객체 State 업데이트 (중요!)
    setFormData({
      ...formData,              // 기존 객체 복사 (스프레드 연산자)
      [fieldName]: fieldValue   // 특정 필드만 업데이트
    });
    
    /*
      예시: name input에 "철수" 입력 시
      1. fieldName = "name", fieldValue = "철수"
      2. setFormData({
           ...formData,        // { name: "", age: "", email: "" } 복사
           ["name"]: "철수"    // name 필드만 "철수"로 변경
         })
      3. 결과: { name: "철수", age: "", email: "" }
    */
  };
  
  return (
    <div>
      <input
        name="name"              // ⚠️ 중요! 객체의 키와 일치해야 함
        value={formData.name}    // formData 객체의 name 속성
        onChange={handleChange}   // 같은 함수 사용
        placeholder="이름"
      />
      
      <input
        name="age"               // name 속성이 객체 키와 일치
        value={formData.age}
        onChange={handleChange}   // 같은 함수 사용
        placeholder="나이"
      />
      
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}   // 같은 함수 사용
        placeholder="이메일"
      />
      
      {/* 현재 상태 확인 */}
      <div>
        <h3>입력된 정보:</h3>
        <pre>{JSON.stringify(formData, null, 2)}</pre>
        {/*
          JSON.stringify 설명:
          1. formData 객체를 문자열로 변환
          2. null = 변환 함수 없음
          3. 2 = 들여쓰기 2칸 (보기 좋게)
        */}
      </div>
    </div>
  );
}


8️⃣ useRef 완벽 이해 - DOM 직접 제어

useRef 기본 사용법

import { useRef, useState } from "react";

function RefExample() {
  // 🔍 State 선언
  const [text, setText] = useState("");
  
  // 🔍 Ref 생성
  const inputRef = useRef();
  /*
    useRef() 실행 결과:
    inputRef = {
      current: undefined  // 처음엔 undefined, 나중에 DOM 요소가 들어감
    }
  */
  
  // 🔍 버튼 클릭 함수
  const handleClick = () => {
    // inputRef.current = 실제 <input> DOM 요소
    console.log("Ref 객체:", inputRef);
    console.log("DOM 요소:", inputRef.current);
    console.log("입력값:", inputRef.current.value);
    
    // 🔍 DOM 직접 제어 예시들
    
    // 1. 포커스 주기
    inputRef.current.focus();
    // 입력창에 커서가 깜빡이게 됨
    
    // 2. 스타일 직접 변경
    inputRef.current.style.backgroundColor = "yellow";
    inputRef.current.style.border = "2px solid red";
    
    // 3. 값 직접 변경 (State 사용 안하고)
    // inputRef.current.value = "직접 변경!";
    
    // 4. 선택 상태 만들기
    inputRef.current.select();
    // 입력창의 텍스트가 전체 선택됨
    
    // 5. 속성 변경
    inputRef.current.placeholder = "포커스됨!";
    
    // 6. 비활성화
    // inputRef.current.disabled = true;
  };
  
  // 🔍 컴포넌트 마운트 시 자동 포커스
  useEffect(() => {
    // 페이지 로드되자마자 입력창에 포커스
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []); // 빈 배열 = 처음 한 번만 실행
  
  return (
    <div>
      {/* 🔍 ref 속성으로 연결 */}
      <input
        ref={inputRef}      // ⚠️ 중요! 이 줄이 연결하는 역할
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="페이지 로드 시 자동 포커스"
      />
      {/*
        ref={inputRef} 설명:
        1. React가 이 input 요소를 생성
        2. inputRef.current에 이 요소를 저장
        3. 이제 inputRef.current로 이 input에 접근 가능
      */}
      
      <button onClick={handleClick}>
        포커스 & 스타일 변경
      </button>
    </div>
  );
}

Ref vs State 비교

function RefVsState() {
  // State: 화면 업데이트가 필요한 데이터
  const [stateValue, setStateValue] = useState(0);
  
  // Ref: 화면 업데이트가 필요 없는 데이터
  const refValue = useRef(0);
  
  const increaseState = () => {
    setStateValue(stateValue + 1);
    console.log("State:", stateValue + 1);
    // 컴포넌트 리렌더링 발생! ✅
  };
  
  const increaseRef = () => {
    refValue.current = refValue.current + 1;
    console.log("Ref:", refValue.current);
    // 값은 바뀌지만 리렌더링 안 함! ❌
  };
  
  return (
    <div>
      <h2>State: {stateValue}</h2>      {/* 화면에 표시됨 */}
      <h2>Ref: {refValue.current}</h2>   {/* 화면에 안 바뀜! */}
      
      <button onClick={increaseState}>State +1</button>
      <button onClick={increaseRef}>Ref +1 (화면 안 바뀜)</button>
    </div>
  );
}

9️⃣ 실전 프로젝트: 카운터 앱 완전 분석

App.js - State 관리 중앙본부

// 📁 src/App.js
import { useState } from "react";
import Viewer from "./component/Viewer";
import Controller from "./component/Controller";

function App() {
  // 🔍 State 선언 - 왜 여기에?
  const [count, setCount] = useState(0);
  /*
    Q: 왜 App에서 State를 관리하나요?
    A: Viewer와 Controller 둘 다 count를 사용하기 때문!
       - Viewer: count를 화면에 보여줌
       - Controller: count를 변경함
       
    만약 Viewer에 State가 있다면?
    → Controller가 변경할 방법이 없음 (형제끼리는 직접 통신 불가)
    
    만약 Controller에 State가 있다면?
    → Viewer가 현재 값을 알 방법이 없음
    
    그래서 공통 부모인 App에서 관리!
  */
  
  // 🔍 State 변경 함수
  const handleSetCount = (value) => {
    // value = Controller에서 전달받을 숫자 (+1, -1, +10 등)
    
    // 방법 1: 현재 값 기반
    setCount(count + value);
    
    // 방법 2: 이전 값 기반 (더 안전한 방법)
    // setCount((prevCount) => {
    //   console.log("이전 값:", prevCount);
    //   console.log("더할 값:", value);
    //   return prevCount + value;
    // });
  };
  
  return (
    <div className="App">
      <h1>Simple Counter</h1>
      
      <section>
        {/* 🔍 Props로 데이터 전달 */}
        <Viewer count={count} />
        {/*
          Viewer 컴포넌트에게:
          - count라는 이름으로
          - 현재 State 값(count)을 전달
        */}
      </section>
      
      <section>
        {/* 🔍 Props로 함수 전달 */}
        <Controller handleSetCount={handleSetCount} />
        {/*
          Controller 컴포넌트에게:
          - handleSetCount라는 이름으로
          - State 변경 함수를 전달
          
          이제 Controller에서 이 함수를 호출하면
          App의 State가 변경됨!
        */}
      </section>
    </div>
  );
}

export default App;

Viewer.js - 데이터 표시 담당

// 📁 src/component/Viewer.js

// 🔍 Props 받기 (구조 분해 할당)
const Viewer = ({ count }) => {
  // { count } = props.count를 바로 count 변수로 받기
  
  // 🔍 조건부 스타일링
  let emoji = "";
  if (count > 0) emoji = "😊";        // 양수일 때
  else if (count < 0) emoji = "😢";   // 음수일 때
  else emoji = "😐";                  // 0일 때
  
  return (
    <div>
      <div>현재 카운트:</div>
      <h1>{count}</h1>
      {/*
        렌더링 과정:
        1. App에서 count={10} 전달
        2. Viewer가 { count } = { count: 10 } 받음
        3. {count} = {10} 으로 치환
        4. 화면에 10 표시
      */}
      
      <div style={{ fontSize: "50px" }}>{emoji}</div>
      
      {/* 조건부 렌더링 */}
      {count >= 100 && <p>🎉 100점 돌파! 축하합니다!</p>}
      {count <= -100 && <p>😱 -100 이하입니다!</p>}
    </div>
  );
};

export default Viewer;

Controller.js - 이벤트 처리 담당

// 📁 src/component/Controller.js

// 🔍 Props로 함수 받기
const Controller = ({ handleSetCount }) => {
  // handleSetCount = App에서 전달받은 State 변경 함수
  
  return (
    <div>
      {/* 🔍 각 버튼별 설명 */}
      
      <button onClick={() => handleSetCount(-1)}>
        -1
      </button>
      {/*
        클릭 시 동작:
        1. () => handleSetCount(-1) 실행
        2. handleSetCount(-1) 호출
        3. App의 handleSetCount 함수 실행
        4. App의 setCount(count + (-1)) 실행
        5. count State가 1 감소
        6. App 리렌더링
        7. Viewer도 새로운 count 받아서 리렌더링
      */}
      
      <button onClick={() => handleSetCount(-10)}>
        -10
      </button>
      {/* handleSetCount(-10) → count가 10 감소 */}
      
      <button onClick={() => handleSetCount(-100)}>
        -100
      </button>
      
      <button onClick={() => handleSetCount(+100)}>
        +100
      </button>
      
      <button onClick={() => handleSetCount(+10)}>
        +10
      </button>
      
      <button onClick={() => handleSetCount(+1)}>
        +1
      </button>
      
      {/* ❌ 흔한 실수들 */}
      {/* <button onClick={handleSetCount(-1)}> */}
      {/* 위 코드는 즉시 실행됨! 화살표 함수로 감싸야 함 */}
      
      {/* <button onClick={handleSetCount}> */}
      {/* 이렇게 하면 이벤트 객체가 전달됨, value가 아님! */}
    </div>
  );
};

export default Controller;

🔄 전체 데이터 흐름 완전 분석

시나리오: +10 버튼을 클릭했을 때

/*
📊 단계별 실행 과정:

1️⃣ 사용자가 Controller의 "+10" 버튼 클릭

2️⃣ Controller의 onClick 이벤트 발생
   onClick={() => handleSetCount(+10)}
   
3️⃣ 화살표 함수 실행
   () => handleSetCount(+10)
   
4️⃣ handleSetCount(+10) 호출
   - 이 함수는 Props로 받은 App의 함수
   
5️⃣ App의 handleSetCount 함수 실행
   const handleSetCount = (value) => {  // value = 10
     setCount(count + value);           // count + 10
   }
   
6️⃣ State 업데이트
   setCount(이전값 + 10)
   예: 0 + 10 = 10
   
7️⃣ React가 State 변경 감지
   "아, count가 바뀌었네? 리렌더링해야겠다!"
   
8️⃣ App 컴포넌트 리렌더링
   - 새로운 count 값으로 다시 실행
   - count = 10
   
9️⃣ 자식 컴포넌트들도 리렌더링
   - Viewer는 count={10} 받음
   - Controller는 동일한 함수 받음
   
🔟 화면 업데이트
   - Viewer가 10을 화면에 표시
   - 사용자는 변경된 숫자를 봄
*/

데이터 흐름 규칙 정리

// 📐 리액트의 철칙들

// 1️⃣ Props는 읽기 전용 (수정 불가)
function Wrong({ count }) {
  count = 10;  // ❌ 에러! Props는 수정 불가
}

function Correct({ count, setCount }) {
  setCount(10);  // ✅ 전달받은 함수로 변경
}

// 2️⃣ State는 직접 수정 불가
function Wrong() {
  const [count, setCount] = useState(0);
  count = 10;  // ❌ 이렇게 하면 리렌더링 안 됨
}

function Correct() {
  const [count, setCount] = useState(0);
  setCount(10);  // ✅ setter 함수 사용
}

// 3️⃣ 자식은 부모에게 직접 데이터 전달 불가
// 자식 → 부모: 함수 호출로만 가능
// 부모 → 자식: Props로 전달

// 4️⃣ 형제끼리는 직접 통신 불가
// 반드시 부모를 거쳐야 함 (State 끌어올리기)

💡 자주 하는 실수와 해결법

실수 1: 함수에 괄호 붙이기

// ❌ 잘못된 코드
<button onClick={handleClick()}>클릭</button>
// 페이지 로드하자마자 실행됨!

// ✅ 올바른 코드
<button onClick={handleClick}>클릭</button>
// 클릭할 때만 실행

// ✅ 매개변수가 필요한 경우
<button onClick={() => handleClick(10)}>클릭</button>

실수 2: State 직접 수정

// ❌ 잘못된 코드
const [user, setUser] = useState({ name: "철수", age: 15 });
user.age = 16;  // 직접 수정 → 리렌더링 안 됨!

// ✅ 올바른 코드
setUser({ ...user, age: 16 });  // 새 객체 생성

실수 3: 조건부 렌더링 실수

// ❌ 0이 화면에 보임
{count && <div>카운트: {count}</div>}
// count가 0일 때 0이 화면에 나타남

// ✅ 올바른 코드
{count > 0 && <div>카운트: {count}</div>}
// 또는
{count ? <div>카운트: {count}</div> : null}

🎯 연습 문제 (코드 포함)

1. Toggle 버튼 만들기

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);
  
  const toggle = () => {
    setIsOn(!isOn);  // true ↔ false 전환
  };
  
  return (
    <button 
      onClick={toggle}
      style={{
        backgroundColor: isOn ? "green" : "red",
        color: "white"
      }}
    >
      {isOn ? "켜짐" : "꺼짐"}
    </button>
  );
}

2. 실시간 글자수 세기

function CharacterCounter() {
  const [text, setText] = useState("");
  const maxLength = 100;
  
  return (
    <div>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        maxLength={maxLength}
      />
      <p>
        {text.length} / {maxLength}
        {text.length >= maxLength && " (최대 글자수 도달!)"}
      </p>
    </div>
  );
}

이제 코드의 모든 부분을 완전히 이해하셨을 거예요! 🎉



참고 자료: 이 글은 ‘한입 크기로 잘라 먹는 리액트’를 바탕으로 작성되었습니다.