[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>
);
}
이제 코드의 모든 부분을 완전히 이해하셨을 거예요! 🎉
참고 자료: 이 글은 ‘한입 크기로 잘라 먹는 리액트’를 바탕으로 작성되었습니다.