이 네가지 질문에 대한 답변을 위한 공부이다.
리액트 라이브러리에서의 props 와 state 란?
props는 properties(속성)의 줄임말로, React에서 컴포넌트 간 데이터를 전달할 때 사용하는 객체이다.
정확히 말하면, 부모 컴포넌트가 자식 컴포넌트에게 값을 넘겨줄 때 사용되는 읽기 전용(Read-Only) 데이터이다.
"단방향 전달"
props는 항상 부모 → 자식 방향으로만 전달된다. 자식 컴포넌트는 받은 props를 화면에 출력하거나 로직에 사용할 수 있지만, 그 값을 변경할 수는 없다.
"객체 구조"
React 컴포넌트는 사실상 JavaScript 함수(function) 또는 클래스(class)로 작성된 코드야.
이때 props는 그 함수에 전달되는 하나의 인자(argument)이며, JavaScript 객체(Object)** 형태로 전달된다.
function Greeting(props) {
console.log(typeof props); // "object"
console.log(props); // 예: { name: "민준", age: 25 }
return <div>{props.name}</div>;
}
즉, props는 일반적인 객체처럼 다음과 같은 특징을 그대로 가짐:
- key-value 구조로 되어 있음
- 점 표기법(props.name) 또는 구조 분해 할당(const { name } = props) 가능
- 얕은 복사, Object.keys(), for...in 등 JS의 객체 연산 사용 가능
알다시피 React에서 우리가 사용하는 컴포넌트는 사실 JavaScript 함수이다.
예를 들어:
function Welcome(props) {
return <h1>리액트 세상에 어서오세요, {props.name}님!</h1>;
}
이라는 props 라는 이름의 매개변수를 받는 함수가 있다고 했을 때,
우리는 이 함수를 JSX 문법을 통해 컴포넌트로 사용하며, 사용할때는 아래와 같이 사용한다.
<Welcome name="빈지노" />
그런데 React 는 이 코드를 내부적으로 함수 호출 로 변환해서 처리한다.
실제로는 아래처럼 동작하는 것과 매우 비슷하다.
Welcome({ name: "민준" });
→ 즉, <Welcome name="빈지노" /> 이 JSX 문법의 한 줄이
→ js에서 Welcome() 이라는 함수에 { name: "빈지노" } 객체를 첫 번째 인자로 자동 전달하는 것과 같아.
이 말이 바로
"props는 컴포넌트의 첫 번째 인자로 자동 전달된다"
라는 의미.
"React에서는 모든 props를 하나의 객체로 묶어서 컴포넌트에 넘기기 때문에, 이 객체 하나가 함수의 '첫 번째이자 유일한 인자'가 된다."
그래서 "첫 번째 인자"라는 표현은 기술적으로 정확한 말이고, "단 하나의 인자" 라고 이해해도 무방해보인다.
state는 영어로 상태를 뜻하지만, React에서는 컴포넌트의 "변경 가능한 데이터"를 의미한다. let, const로 변수를 선언하듯이,const [state, setState] = useState({ key: value });
{} 안에 원하는 구조로 값을 정의할 수 있다.class LikeButton extends React.Component {
constructor(props) {
super(props);
this.state = { liked: false };
}
handleClick = () => {
this.setState({ liked: true });
};
}
this.state로 초기값을 정의하고,this.setState()를 써야 한다.import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
useState()를 사용해서 상태를 만들고,set함수를 통해 상태를 변경한다.this.state = { name: "react" }; // ❌ 직접 수정
this.setState({ name: "react" }); // ✅ setState로 수정
setState()나 set함수()로 상태를 바꿔야 컴포넌트가 다시 렌더링된다.UI에 영향을 주는 값이면 state로 관리해야 한다.
예시:
화면에 영향을 주지 않는 임시 값이나 내부 계산값은 state 말고 일반 변수나 ref로 처리하는 게 더 적절하다.
const [items, setItems] = useState([]);
const itemCount = items.length; // 이렇게 계산해서 쓰면 된다
불필요하게 많은 state를 만들면 렌더링이 과하게 일어나서 성능이 떨어질 수 있다.
set함수를 통해 변경해야 한다.이렇게 props와 state에 대해 간단히 알아 보았다.
면접에 나오는 다음 네 질문에 대해 정리하고 블로깅을 마치겠다.
props는 컴포넌트 외부에서 주어진 읽기 전용 데이터,
state는 컴포넌트 내부에서 관리하는 변경 가능한 데이터
직접적으로는 불가능하다.
props는 부모 → 자식 방향으로만 일방향 전달(one-way binding)된다.
하지만 "간접적으로는 가능하다".
→ 방법은? 자식에게 함수를 props로 내려주고, 자식이 그 함수를 호출하는 방식을 사용한다.
// 부모 컴포넌트
function Parent() {
const handleReceive = (data) => {
console.log("자식으로부터 받은 값:", data);
};
return <Child onSend={handleReceive} />;
}
// 자식 컴포넌트
function Child({ onSend }) {
return (
<button onClick={() => onSend("🔥 자식에서 생성한 data")}>
부모에게 전달
</button>
);
}
handleRecieve 함수, aka onSend 에서 '🔥 자식에서 생성한 data' 를 역방향으로 받았으니
결과는 console.log("자식으로부터 받은 값:", 🔥 자식에서 생성한 data"); 가 되어
"자식으로부터 받은 값:", 🔥 자식에서 생성한 data 가 출력된다.
→ 이렇게 하면 자식 컴포넌트에서 부모 컴포넌트로 데이터를 넘기는 것이 가능하다.
불변성(immutability)이란 데이터를 직접 변경하지 않고, 복사본을 만들어 새로운 값을 할당하는 방식을 말한다.
즉, 기존 객체를 그대로 두고 새로운 객체를 만들어 교체하는 것이다.
React는 state가 바뀌었는지를 확인할 때,
객체 내부를 깊게 비교하지 않고 얕은 비교(shallow comparison)로 판단한다.
즉, 참조값(주소값)만 보고 변경 여부를 판단한다.
const obj1 = { a: 1 };
const obj2 = { a: 1 };
obj1 === obj2; // false (서로 다른 객체)
→ 이처럼 객체가 바뀌었는지를 확인할 때는 주소값만 본다.
→ 그래서 객체 내부 값을 바꿔도 주소가 같으면 React는 "변화 없음"으로 판단해서 렌더링하지 않는다.
const [form, setForm] = useState({ name: "", email: "" });
const handleNameChange = (e) => {
form.name = e.target.value; // ❌ 직접 수정
setForm(form); // ❌ 참조값 그대로라 변화 감지 안 됨
};
→ form 객체의 주소는 그대로이기 때문에, React는 변화된 줄 모름
→ 렌더링 안 되거나 UI 반영 안 될 수 있음
// 입력 폼 상태 관리
const [form, setForm] = useState({ name: "", email: "" });
// 이름 입력값 변경 시
const handleNameChange = (e) => {
setForm({
...form,
name: e.target.value,
});
};
→ 기존 form 객체를 복사하고, name만 덮어써서 새로운 객체로 교체함
React는 참조값(주소)을 기준으로 상태 변경을 판단하기 때문에,
기존 값을 직접 수정하지 않고 새로운 객체나 배열을 만들어 교체하는 방식(불변성 유지)을 사용해야 한다.
리듀서는 상태(state)를 업데이트하는 함수이다.
현재 상태(state)와 액션(action)을 받아서,
새로운 상태를 반환(return)하는 역할을 한다.
리듀서는 직접 상태를 변경하지 않고,
새로운 상태를 만들어 반환해야 한다. → 불변성 유지!
이름은 배열의 .reduce() 함수에서 따온 개념이다.
[1, 2, 3].reduce((acc, cur) => acc + cur, 0); // 결과: 6
→ 현재 상태(acc)와 새로운 값(cur)을 받아서
→ 누적된 결과를 만들어낸다는 점에서 유사하다.
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
사용 예 :
<button onClick{() => dispatch({ type: "increment" })}>+</button>
-> dispatch가 reducer에 `{ type: "increment" } 를 전달하고, reducer는 새로운 state를 만들어서 반환한다.
| 상황 | useReducer가 많이 쓰이는 이유 |
|---|---|
| 복잡한 폼(form) 입력 상태 관리 | name, email, password 등 필드가 많을 때 |
| 상태 변경 액션이 여러 개일 때 | add, remove, toggle, reset 등 |
| 컴포넌트가 크고 상태 변경 로직을 분리하고 싶을 때 | reducer 함수로 바깥에 빼두면 가독성 👍 |
| Redux를 흉내낸 구조가 필요할 때 | 작은 규모의 전역 상태처럼 씀 |
다시 질문으로 돌아와서,
리듀서 내부에서 불변성을 지키는 이유도 동일하다.
리듀서는 “규칙”만 있는 함수라 개발자가 그 안에서 state.value = 123 같이 직접 상태를 바꾸면,
React나 Redux는 그걸 감지하지 못하거나, 잘못 작동할 수 있다.
다시 한번 정리하지만 React나 상태의 참조값이 바뀌었는지를 기준으로 리렌더링 여부를 판단하기 때문에,
리듀서 내부에서 불변성을 지키지 않으면 변경을 감지하지 못하거나, 예기치 않게 동작할 수 있다.
우선 전개연산자의 단점을 보자.
const nextState = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
name: "민준",
},
},
};
... 가 반복되어 코드가 길고 복잡해진다.극복 방법으로는 Immer.js 라는 녀석이 있다!
"직접 수정하는 것처럼 작성하지만 내부적으로는 불변성을 지켜주는" 라이브러리다."
전개연산자 코드이다.
const nextState = {
...state,
user: {
...state.user,
name: "프렌치 토스트",
},
};
Immer 사용 코드이다.
import produce from "immer";
const nextState = produce(state, (draft) => {
draft.user.name = "프렌치 토스트";
});
위 꼴로 Immer.js 를 사용할 수 있는데, Immer 를 사용하면 마치 직접 수정하듯이 수정해도 내부적으로는 "복사 + 변경 추적 + 새로운 객체 생성"을 자동으로 해준다.
정리하자면,
전개 연산자는 얕은 복사에는 간편하지만,
중첩된 객체를 업데이트할 땐 코드가 길어지고 실수하기 쉽다.
이를 해결하기 위해 Immer.js를 사용하면,
직접 수정하는 것처럼 작성하면서도 내부적으로 불변성이 유지되기 때문에
코드 가독성과 안정성이 모두 좋아진다.