쇼핑몰에서 옷을 주문하는 모습을 상상해 보세요. 원하는 옷을 장바구니에 담고 결제를 하려고 보니 다른 옷이 더 마음에 듭니다. 장바구니에 담은 옷을 빼고, 새로운 옷을 담아 결제를 합니다.
이 상황에서 장바구니에 담기는 옷은 바뀌고 내가 결제해야 할 금액도 즉각적으로 변경됩니다. 이를 우리는 장바구니의 상태가 변한다고 말할 수 있으며, 결제 페이지에 변경된 장바구니의 상태를 전달해야 합니다.
State
Props vs State
ex) 어떤것이 Props 또는 State에 적합할까요?
Props
State
컴포넌트의 속성(property)을 의미합니다.
props는 성별이나 이름처럼 변하지 않는 외부로부터 전달받은 값으로, 웹 애플리케이션에서 해당 컴포넌트가 가진 속성에 해당합니다.
부모 컴포넌트(상위 컴포넌트)로부터 전달받은 값
React 컴포넌트는 JavaScript 함수와 클래스로, props를 함수의 전달인자(arguments)처럼 전달받아 이를 기반으로 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환합니다. 따라서, 컴포넌트가 최초 렌더링 될 때 화면에 출력하고자 하는 데이터를 담은 초깃값으로 사용할 수 있습니다.
객체 형태입니다.
props로 어떤 타입의 값도 넣어 전달할 수 있도록 props는 객체의 형태를 가집니다.
props는 읽기 전용입니다.
props는 성별이나 이름처럼 외부로부터 전달받아 변하지 않는 값입니다. 그래서 props는 함부로 변경될 수 없는 읽기 전용(read-only) 객체입니다. 함부로 변경되지 않아야 하기 때문입니다.
props를 사용하는 방법은 아래와 같이 3단계 순서로 나눌 수 있습니다.
위 단계에 맞추어 props를 사용하기 위해 우선 Parent 와 Child 라는 컴포넌트를 선언하고, Parent 컴포넌트 안에 Child 컴포넌트를 작성합니다.
function Parent() {
return (
<div className="parent">
<h1>I'm the parent</h1>
<Child />
</div>
);
};
function Child() {
return (
<div className="child"></div>
);
};
컴포넌트를 만들었으니 이제 전달하고자 하는 속성을 정의해 봅시다. HTML에서 속성과 값을 할당하는 방법과 같습니다. 아래의 코드에서는 a 요소의 href 속성에 "www.codestates.com"라는 값을 주었습니다.
<a href="www.codestates.com">Click me to visit Code States</a>
React에서 속성 및 값을 할당하는 방법도 이와 유사합니다. 다만, 전달하고자 하는 값을 중괄호 {}를 이용하여 감싸주면 됩니다.
<Child attribute={value} />
위 방법을 이용하여 text라는 속성을 선언하고, 이 속성에 "I'm the eldest child"라는 문자열 값을 할당하여 Child 컴포넌트에 전달해 봅시다.
<Child text={"I'm the eldest child"} />
자, 이제 Parent 컴포넌트에서 전달한 "I'm the eldest child"라는 문자열을 Child 컴포넌트에서 받아 봅시다. 방법은 간단합니다. 함수에 인자를 전달하듯이 React 컴포넌트에 props를 전달하면 되고, 이 props가 필요한 모든 데이터를 가지고 오게 됩니다.
function Child(props) {
return (
<div className="child"></div>
);
};
props를 전달받았으니, 마지막으로 이 props를 렌더링해 봅시다. props를 렌더링하려면 JSX 안에 직접 불러서 사용하면 됩니다.
다만, props는 객체라고 하였고, 이 객체의 { key : value }는 Parent 컴포넌트에서 정의한 { attribute : value }의 형태를 띠게 됩니다.
따라서 JavaScript에서 객체의 value에 접근할 때 dot notation을 사용하는 것과 동일하게 props의 value 또한 dot notation으로 접근할 수 있습니다. 아래와 같이 props.text를 JSX에 중괄호와 함께 작성하면 잘 작동합니다.
function Child(props) {
return (
<div className="child">
<p>{props.text}</p>
</div>
);
};
import "./styles.css";
function Parent() {
return (
<div className="parent">
<h1>I'm the parent</h1>
<Child text={"I'm the eldest child"} />
<Child text2={"I'm the second child"} />
</div>
);
}
function Child(props) {
console.log("props : ", props);
return (
<div className="child">
<p>{props.text}</p>
<p>{props.text2}</p>
</div>
);
}
export default Parent;
props를 전달하는 또 다른 방법으로 여는 태그와 닫는 태그의 사이에 value를 넣어 전달하는 방법이 있습니다.
이 경우 props.children을 이용하면 해당 value에 접근하여 사용할 수 있습니다. 위의 스니펫을 아래 코드로 변경 후 props.children의 실행 결과를 직접 확인해 보세요.
function Parent() {
return (
<div className="parent">
<h1>I'm the parent</h1>
<Child>I'm the eldest child</Child>
</div>
);
};
function Child(props) {
return (
<div className="child">
<p>{props.children}</p>
</div>
);
};
컴포넌트 내부에서 변할 수 있는 값
쇼핑몰 장바구니를 예로 들어보겠습니다. 사용자는 구매할 물건과 당장은 구매하지 않을 물건을 체크박스에 체크하여 구분 짓습니다.
이를 장바구니 내에서의 상태로 구분해 본다면 check 된 상태와 check 되지 않은 상태입니다.
이처럼 컴포넌트 내에서 변할 수 있는 값, 즉 상태는 React state로 다뤄야 합니다.
import React, { useState } from "react";
import "./styles.css";
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
const handleChecked = (event) => {
setIsChecked(event.target.checked);
};
return (
<div className="App">
<input type="checkbox" checked={isChecked} onChange={handleChecked} />
<span>{isChecked ? "Checked!!" : "Unchecked"}</span>
</div>
);
}
export default CheckboxExample;
React에서는 state를 다루는 방법 중 하나로 useState라는 특별한 함수를 제공합니다. 이 함수의 사용 방법과 작동 방식을 위의 체크박스 예로 들어 살펴보겠습니다.
mport { useState } from "react"
function CheckboxExample() {
// 새로운 state 변수를 선언하고, 여기서는 이것을 isChecked라 부르겠습니다.
const [isChecked, setIsChecked] = useState(false);
}
function CheckboxExample() {
// 1번 코드를 풀어쓰면
const [isChecked, setIsChecked] = useState(false); // 1번
//...
// 2번 코드와 같습니다.
const stateHookArray = useState(false); // 2번
const isChecked = stateHookArray[0];
const setIsChecked = stateHookArray[1];
}
const [state 저장 변수, state 갱신 함수] = useState(상태 초기 값);
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
// const [state 저장 변수, state 갱신 함수] = useState(state 초깃값);
isChecked : state를 저장하는 변수
setIsChecked : state isChecked를 변경하는 함수
useState : state hook
false : state 초깃값
<span>{isChecked ? "Checked!!" : "Unchecked"}</span>
state를 갱신하려면 state 변수를 갱신할 수 있는 함수인 setIsChecked를 호출합니다.
이번 예시의 경우, input[type=checkbox] JSX 엘리먼트의 값 변경에 따라서 isChecked가 변경되어야 합니다. 브라우저에서 checked로 값이 변경되었다면, React의 isChecked도 변경되어야겠죠?
input[type=checkbox] 엘리먼트의 값이 변경되면 onChange 이벤트가 발생하고, 이벤트 핸들러 함수가 작동되는 패턴은 DOM을 다뤄보시면서 익숙해지셨죠? 유효성 검사 스프린트에서 input[type=text] 엘리먼트의 값이 변경될 때, 이벤트 핸들러 함수를 작동시키는 패턴을 복습해 보세요.
React도 마찬가지입니다. 사용자가 체크박스 값을 변경하면 onChange 이벤트가 이벤트 핸들러 함수인 handleChecked를 호출하고, 이 함수가 setIsChecked를 호출하게 됩니다. setIsChecked가 호출되면 호출된 결과에 따라 isChecked 변수가 갱신되며, React는 새로운 isChecked 변수를 CheckboxExample 컴포넌트에 넘겨 해당 컴포넌트를 다시 렌더링 합니다.
function CheckboxExample() {
const [isChecked, setIsChecked] = useState(false);
const handleChecked = (event) => {
setIsChecked(event.target.checked);
};
return (
<div className="App">
<input type="checkbox" checked={isChecked} onChange={handleChecked} />
<span>{isChecked ? "Checked!!" : "Unchecked"}</span>
</div>
);
}
React 컴포넌트는 state가 변경되면 새롭게 호출되고, 리렌더링 됩니다.
아래 예시의 체크박스를 눌러보시면, 누를 때마다 콘솔에 "rerendered?"가 찍히는 것을 확인하실 수 있습니다. 즉, 컴포넌트의 상태가 변경될 때마다 새롭게 호출되고, 리렌더링 됩니다.
import React, { useState } from "react";
import "./styles.css";
function CheckboxExample() {
console.log("rerendered?");
const [isChecked, setIsChecked] = useState(false);
const handleChecked = (event) => {
setIsChecked(event.target.checked);
};
return (
<div className="App">
<input type="checkbox" checked={isChecked} onChange={handleChecked} />
<span>{isChecked ? "Checked!!" : "Unchecked"}</span>
</div>
);
}
export default CheckboxExample;
React의 이벤트 처리(이벤트 핸들링; Event handling) 방식은 DOM의 이벤트 처리 방식과 유사합니다. 단, 몇 가지 문법 차이가 있습니다.
React에서 이벤트는 소문자 대신 카멜 케이스(camelCase)를 사용합니다.
JSX를 사용하여 문자열이 아닌 함수로 이벤트 처리 함수(이벤트 핸들러; Event handler)를 전달합니다.
// 예를 들어 HTML에서 이벤트 처리 방식이 아래와 같다면,
<button onclick="handleEvent()">Event</button>
// React의 이벤트 처리 방식은 아래와 같습니다.
<button onClick={handleEvent}>Event</button>
input, textarea, select와 같은 폼(Form) 엘리먼트는 사용자의 입력값을 제어하는 데 사용됩니다.
React에서는 이러한 변경될 수 있는 입력값을 일반적으로 컴포넌트의 state로 관리하고 업데이트합니다.
onChange 이벤트가 발생하면 e.target.value를 통해 이벤트 객체에 담겨있는 input 값을 읽어올 수 있습니다.
컴포넌트 return 문 안의 input 태그에 value와 onChange를 넣어주었습니다.
onChange는 input의 텍스트가 바뀔 때마다 발생하는 이벤트입니다. 이벤트가 발생하면 handleChange 함수가 작동하며, 이벤트 객체에 담긴 input 값을 setState를 통해 새로운 state로 변경합니다.
function NameForm() {
const [name, setName] = useState("");
const handleChange = (e) => {
setName(e.target.value);
}
return (
<div>
<input type="text" value={name} onChange={handleChange}></input>
<h1>{name}</h1>
</div>
)
};
onClick 이벤트는 말 그대로 사용자가 클릭이라는 행동을 하였을 때 발생하는 이벤트입니다.
버튼이나 a tag를 통한 링크 이동 등과 같이 주로 사용자의 행동에 따라 애플리케이션이 반응해야 할 때 자주 사용하는 이벤트입니다.
그럼 위의 onChange 예시에 버튼을 추가하여 버튼 클릭 시 input tag에 입력한 이름이 alert을 통해 알림 창이 팝업 되도록 코드를 추가해 보겠습니다.
function NameForm() {
const [name, setName] = useState("");
const handleChange = (e) => {
setName(e.target.value);
}
return (
<div>
<input type="text" value={name} onChange={handleChange}></input>
<button onClick={alert(name)}>Button</button>
<h1>{name}</h1>
</div>
);
};
위와 같이 onClick 이벤트에 alert(name) 함수를 바로 호출하면 컴포넌트가 렌더링 될 때 함수 자체가 아닌 함수 호출의 결과가 onClick에 적용됩니다.
때문에 버튼을 클릭할 때가 아닌, 컴포넌트가 렌더링 될 때에 alert이 실행되고 따라서 그 결과인 undefined(함수는 리턴 값이 없을 때 undefined를 반환합니다.)가 onClick에 적용되어 클릭했을 때 아무런 결과도 일어나지 않습니다.
따라서 onClick 이벤트에 함수를 전달할 때는 함수를 호출하는 것이 아니라 아래와 같이 리턴문 안에서 함수를 정의하거나 리턴문 외부에서 함수를 정의 후 이벤트에 함수 자체를 전달해야 합니다.
// 함수 정의하기
return (
<div>
...
<button onClick={() => alert(name)}>Button</button>
...
</div>
);
};
// 함수 자체를 전달하기
const handleClick = () => {
alert(name);
};
return (
<div>
...
<button onClick={handleClick}>Button</button>
...
</div>
);
};
import "./styles.css";
import React, { useState } from "react";
function NameForm() {
const [name, setName] = useState("");
const handleChange = (e) => {
setName(e.target.value);
};
const handleClick = () => {
alert(name);
};
return (
<div className="App">
<h1>Event handler practice</h1>
<input type="text" value={name} onChange={handleChange}></input>
<button onClick={handleClick}>Button</button>
{/* <button onClick={() => alert(name)}>Button</button> */}
<h3>{name}</h3>
</div>
);
}
export default NameForm;
인터넷 이전에 사람들은 어떻게 정보를 저장하고 전달할 수 있었을까요? 종이에 글을 적어서 문서를 만들고, 이를 우편에 부쳐서 전달할 수 있었습니다. 이제는 트위터와 같은 SNS를 통해서 클릭 한 번으로 사진부터 동영상까지 보낼 수 있지만요.
우편 부칠 때 가장 중요한 정보는 무엇인가요? 보내는 사람, 받는 사람, 주소, 종이의 내용 등이 중요한 정보입니다. 이 내용은 바뀔 수 있나요? 우편의 종류에 따라 다르지만 바뀔 수 있습니다. 김코딩이 박해커에게 안부 인사를 편지로 하는 경우 보내는 사람은 김코딩, 받는 사람은 박해커가 되고 박해커가 송기획에게 제안서를 보내면 보내는 사람은 박해커, 받는 사람은 송기획이 됩니다.
트윗도 마찬가지입니다. 다만, 트윗은 받는 사람이 정해져 있습니다. 팔로워가 받는 사람으로 정해져 있죠. 보내는 사람의 경우, 누가 작성하는가에 따라 변경될 수 있습니다. 트윗에도 우편 봉투가 있다면, 트윗 우편 봉투의 상태가 됩니다. 트윗 우편 봉투의 내용도 마찬가지로 변경될 수 있는 값, 상태가 됩니다.
우편 봉투에 해당하는 위 그림을 하나의 트윗 전송 폼 컴포넌트라고 합시다. state는 무엇이 되어야 하나요? "변경될 수 있는 값"인 보내는 사람(username)과 보낼 내용(tweet)이 state가 되어야 합니다.
React에서는 이렇게 상태에 해당하는 데이터를 state로 따로 관리하고 싶어 합니다. 이렇게 React가 state를 통제할 수 있는 컴포넌트를 Controlled Component라고 합니다.
어떻게 React가 state를 통제할 수 있을까요? input에 값 입력 시, state도 그때그때 바뀌면(onChange) 됩니다. 그리고 이 변경된 state와 input의 value 또한 같게 작성해야 합니다.
React의 개발 방식의 가장 큰 특징은 페이지 단위가 아닌, 컴포넌트 단위로 시작한다는 점입니다.
상향식(bottom-up)으로 앱을 만듭니다. 이것의 가장 큰 장점은 테스트가 쉽고 확장성이 좋습니다.
그래서 여러분이 기획자나 PM, 또는 UX 디자이너로부터 앱의 디자인을 전달받고 나면, 이를 컴포넌트 계층 구조로 나누는 것이 가장 먼저 해야 할 일입니다.
단일 책임 원칙에 따른 구분입니다. 하나의 컴포넌트는 한 가지 일만 합니다.
컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있습니다.
데이터를 전달하는 주체는 부모 컴포넌트가 됩니다. 이는 데이터 흐름이 하향식(top-down) 임을 의미합니다.
이 원칙은 매우 중요합니다. 얼마나 중요하냐면, 단방향 데이터 흐름(one-way data flow)이라는 키워드가 React를 대표하는 설명 중 하나일 정도입니다.
또한 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못합니다.
모든 데이터를 상태로 둘 필요는 없습니다. 사실 상태는 최소화하는 것이 가장 좋습니다.
상태가 많아질수록 애플리케이션은 복잡해집니다. 어떤 데이터를 상태로 두어야 하는지는 다음 세 가지 질문을 통해 판단해 보세요.
상태가 특정 컴포넌트에서만 유의미하다면, 특정 컴포넌트에만 두면 되니까 크게 어렵지 않지만, 만일 하나의 상태를 기반으로 두 컴포넌트가 영향을 받는다면 이때에는 공통 소유 컴포넌트를 찾아 그곳에 상태를 위치해야 합니다.
즉, 두 개의 자식 컴포넌트가 하나의 상태에 접근하고자 할 때는 두 자식의 공통 부모 컴포넌트에 상태를 위치해야 합니다.
React에서 데이터를 다룰 때는 컴포넌트들 간의 상호 관계와 데이터의 역할, 데이터의 흐름을 고려하여 위치를 설정해야 합니다.