들어가기 전
항해 플러스를 신청한 이유는 React를 흡수하기 위해서다.
지금 프론트엔드 시장에선 React는 default기 때문에 시작하지 않으면 난 바보가 될 것 같았다.
그렇다고 독학으로 시작하기엔 React는 진입장벽이 Vue보다 높고, 자유도도 또한 높기 때문에 어떤식으로 코드를 짜야할 지도 막막했다.
항해 플러스를 마칠 때 리액트 챌린저까진 아니더라도 플래티넘이 되게끔 목표로 잡고 있다.
본격적으로 항해 플러스가 시작되기 전에 React의 기본적인 내용이라도 알고 가지 않으면 자존감이 맨틀을 뚫고 내핵으로 향할 것이 분명했기 때문에 사전 스터디를 신청했다.
최근에 강의를 보면서 React를 맛봤기 때문에 아마 기초적인 내용은 머릿속에 맴돌고 있을 것이니 차근차근 시작해 보자.
1. React 소개
1.1 React의 의미
React를 톺아보기전에, 왜 이름 React로 지었는지 궁금하지 않을 수 없다.
이름이란 무언가의 정체성을 상징하여 가리키기 위해 고유하게 지은 말인데, React를 번역하면 반응하다 라는 뜻이다.
그렇다면 무엇에 반응하는 것일까?
아마 data에 반응하고, 이 반응으로 화면을 수정한다라는 의미에서 React로 이름을 지었지 않았을까 한다.
기존에 jquery 시절에는 화면을 DOM을 사용해서 직접 사용했지만, React는 화면 중심이 아닌 data 중심으로
돌아가기 때문에 코드를 짤 때 화면 중심이 아닌 data를 중심으로 사고하는 것이 좋을 것 같다.
1.2 SPA 아키텍처
이번에 학습할 React는 SPA방식이라는 것은 지나가던 강아지도 알고 있다.
이름 그대로 페이지가 하나인 애플리케이션이며, 반대로는 MPA이 있다.
사실 이 두개중 뭐하나만 꼽으라한다면 꼽을 순 없다.
중요한 내용이니 두 방식에 중요한 개념만 빠르게 읊고 가자.
SPA(Single Page Applaction)
SPA는 주로 CSR(Client Side Rendering)방식을 사용하며, 이는 아래와 같은 장단점이 있다.
*CSR이란?
웹 브라우저에서 JS를 사용하여 웹페이지를 렌더링하는 방식이다.
즉, 사용자의 브라우저가 서버로부터 데이터를 받아와서 웹 페이지를 직접 생성한다.
● 동작 과정
1. 사용자가 웹사이트에 접속하면 클라이언트는 서버에 컨텐츠 요청.
2. 서버는 빈 HTML을 클라이언트에 전달함
3. 클라이언트가 HTML 문서에 포함된 함께 정적 리소스(CSS, 이미지 등), 번들 자바스크립트 파일을 다운로드함.
4. 클라이언트에서 번들 자바스크립트 파일을 실행. 자바스크립트에 의해 페이지가 렌더링되며, 이 때 사용자는 렌더링된 페이지를 볼 수 있음.(이때 API에서 데이터를 받아오지 않았기 때문에 로딩 혹은 임시 데이터가 표출됨.)
5. API 요청을 수행하여 동적 컨텐츠를 가져오고 파싱하여 최종 렌더링함.
6. 사용자가 페이지를 이동할 경우, 서버에 추가 요청하지 않고 이미 다운로드 된 JS를 이용해 렌더링함.
장점
● 리렌더링이 되지 않기 때문에 부드러운 페이지 전환이 가능해 사용자 경험이 매끄럽다.
● 페이지의 일부만 바뀌는 것이기 때문에 반응이 빠르다.
● 서버에게 정적 리소스를 한번만 요청하고, 받은 데이터는 전부 저장해 놓기때문에 오프라인에서도 사용가능함.
● UI를 재사용 가능한 컴포넌트 단위로 개발하여 코드의 유지 보수성, 확장성을 높임.
단점
● 초기에 모든 정적 리소스를 한번에 다운로드 받기 때문에 초기 구동 속도가 느림.
● 페이지가 하나이기 때문에 JS를 읽지 못하는 검색 엔진에 대해서는 크롤링이 되지 않아 SEO 최적화가 어려움.
● SPA는 최신 JS 기능을 사용하므로 오래된 브라우저에서는 제대로 동작하지 않을 수 있음.
MPA(Multi Page Applaction)
MPA는 SSR(Server Side Rendering)을 사용하며 아래와 같은 장단점이 있다.
*SSR이란?
서버쪽에서 렌더링하여 화면을 보여주는 방식이다.
서버로부터 만들어진 HTML을 받아와 화면을 그린다.
● 동작 과정
1. 사용자가 웹사이트에 요청을 보냄
2. 서버는 필요한 데이터를 얻어와 모두 삽입하고 CSS까지 모두 적용해 렌더링 준비를 마친 HTML과 JS를 클라이언트에 전달 함.
3. 클라이언트는 전달 받은 페이지를 띄우고, JS를 다운로드하고 HTML에 실행 시킴.
4. 페이지를 이동하거나 새로고침하면 전체 페이지를 다시 렌더링 함.
장점
● 각 페이지가 독립적인 URL과 컨텐츠를 가지므로 검색 엔진이 페이지를 쉽게 크롤링하고 색인할 수 있어 SEO에 용이하다.
● 오래된 브라우저에서도 잘 작동하며, 특별한 기술이나 라이브러리가 필요하지 않을 수 있음.
● 민감한 데이터나 로직을 서버 측에서 처리하고 클라이언트에게는 필요한 정보만 전달하여 보안을 강화할 수 있음.
단점
● 새로운 페이지를 로드 할 때마다 서버에 요청을 보내고 HTML, CSS, JavaScript등 리소스를 다시 당누로드 해야하므로 페이지 이동 속도가 느려짐.
● 사용자의 모든 요청을 서버가 처리해야 하므로 트래픽이 많은 경우 서버에 부담이 될 수 있다.
● 페이지 이동 시 화면 전체가 깜빡이며 다시 로딩되므로 사용자 경험이 매끄럽지 못하다.
각각의 장단점이 있으며, 장단점을 체크하며 상황에 따라 맞는 방식을 채택하면 된다.
예를 들자면 아마존 같이 여러 제품이나 서비스를 다루는 전자 상거래 상점 및 많은 양의 콘텐츠가 필요하거나, SEO가 중요하다면 MPA를 채택하는게 좋을 것이고, 많은 상호작용이 필요한 소셜처럼 반응 속도를 필요로 한다면 SPA를 채택하면 좋을 것이다.
1.3 우리는 왜 React를 배워야 하는가
우리는 유행을 따라야 할 권리가 있다.
바보 같이 Vue로 시작한 나의 개발은 당장 먹고 살 땐 문제는 없겠지만 이직 준비를 하는 나에겐 크나큰 문제기도 하다.
채용 공고를 정독하다 보면 Vue를 사용하는 회사는 극히 소수이고 끽해봐야 물건찍어내는 SI 회사들이 대부분이다.
물론 SI가 나쁘다는 것은 아니긴 하다. 나는 좀 더 많은 경험을 하고 싶기 때문에 스킬 스펙트럼을 더욱 더 늘려야만 한다.
2. React에서 자주 사용되는 필수 ES6 문법
총 쏘는 법을 배우려면 총기를 먼저 배워야 하듯이, React를 배우려면 JS를 배워야 한다.
React 개발에 필요한 ES6 문법을 살펴보자.
2.1 변수와 상수
변수 | 상수 | ||
var | let | const | |
재선언 | O | X | X |
재할당 | O | O | X |
스코프 | 함수 레벨 스코프 | 블록 레벨 스코프 | 블록 레벨 스코프 |
* scope란?
scope란 유효한 참조 범위를 뜻하며, 변수에 접근할 수 있는 범위
스코프는 전역 스코프(Global Scope), 지역 스코프(Local Scope) 두가지로 나뉜다.

말 그대롭니다. 전역에 살아서 전역 스코프 지역에 살아서 지역 스코프.
전역 스코프는 JS 최상위 레벨에 해당하는 스코프로 함수 어디에서든 접근이 가능하며, 지역 스코프는 특정 함수에 해당하는 스코프로 함수 자신과 하위 함수에서만 접근이 가능하다.
* 전역 스코프 중요 내용
var는 우리가 아는 Global Scope에 window 포함 되지만 let, const는 Script Scope에 포함된다.
지역 스코프
함수 레벨 스코프(funtion-level scope)
함수 내에서 선언된 변수는 함수 내에서만 유효하며 외부에선 참조할 수 없다.
// 함수 레벨 스코프
var a = "전역 스코프";
function foo() {
var a = "지역 함수";
console.log(a); // 지역 함수
if(true) {
var a = "지역 블록"
}
console.log(a) // 지역 블록
}
foo();
console.log(a) // 전역 스코프
블록 레벨 스코프(block-level scope)
블록 내에서 선언된 변수는 블록 내에서만 유효하며 외부에선 참조할 수 없다.
// 블록 레벨 스코프
function foo() {
let a = "1";
const c = "2"
if(true) {
let a = "3"
const c = "4"
console.log(a, c) // 3, 4
}
console.log(a, c) // 1, 2
}
foo();
우리가 잘 사용하는 함수, if문, for문 등 을 블록 범위라고 한다.
잘보면 둘의 차이점은 if문(블록) 바로 다음에 나오는 콘솔에서 보인다.
함수 레벨 스코프를 보면 foo 함수에서 변수를 선언하자마자 콘솔을 찍으면 당연하게도 "지역 함수"가 찍힌다.
if문 안에서 변수를 재선언 하고 콘솔을 찍어보니, 블록 안에서 변경된 변수가 찍힌다.
블록 레벨 스코프에선 foo 함수에서 선언하고 if문 안에서 재선언 했더니 함수 레벨 스코프와 다르게 처음 선언한 값이 찍혔다.
여기서 알게 된 것은 let, const는 함수든 뭐든 그 안에서만 참조할 수 있으며, 외부에선 참조가 불가능하다는 점이다.
넘어가자.
2.2 Object 선언, 단축 속성, 객체 복사
Object는 마치 커플처럼 key와 value가 한 쌍이다. (부럽진 않다.)
선언 및 단축 속성
const name = '수민';
const age = 28;
const person = {
name,
age,
company: 'sparta coding club',
doSomething: function(){},
}
key와 value가 같다면, 생략이 가능하다.
객체 복사
const obj1 = { value1: 10 };
const obj2 = obj1; // 얕은 복사
const obj3 = JSON.parse(JSON.stringify(obj1)) // 깊은 복사
obj1.value1 += 1;
console.log(`obj1:`, obj1); // 11
console.log(`obj2:`, obj2); // 11
console.log(`obj3:`, obj3); // 10
obj1.value1 += 1;
console.log(`obj1:`, obj1); // 12
console.log(`obj2:`, obj2); // 12
console.log(`obj3:`, obj3); // 10
얕은 복사를 사용했을때 변수도 같이 변하는 것을 볼 수 있다. 이 부분은 의도치 않은 변경이 일어날 수 있으니 주의 하자.
* 얕은 복사, 깊은 복사란?
얕은 복사는 객체의 참조 값(주소 값)을 복사하고, 깊은 복사는 객체의 실제 값을 복사한다.
JS는 원시 값과 참조 값 두 가지 타입이 존재하는데, 원시 값은 새로운 메모리 공간에 독립적인 값으 저장하기 때문에 깊은 복사가 되고 참조 값은 얕은 복사가 된다.
위 예제에서 봤듯이 차이점은 원본이 바뀌면 얕은 복사는 복사본도 같이 변경되지만, 깊은 복사는 변경되지 않는 다는 점이다.
2.2 Template Literals
// 일반 텍스트
`string text`
// 멀티라인
`string text line 1
string text line 2`
// 플레이스 홀더를 이용한 표현식
`string text ${expression} string text`
2.3 배열/객체 비구조화
객체 구조분해 할당
const person = {
name: '수민',
age: '28'
}
const { name, age } = person;
console.log(`${name}님, ${age}살이시네요!`);
저장된 값이 상응하는 변수에 할당된다. 순서는 중요하지 않다.
const person = {
name: '수민',
age: '28'
puppy: '미니'
}
// { 객체 프로퍼티: 목표 변수 }
const { name: a, age, puppy: love } = person;
console.log(`${a}님, ${age}살이시네요!, ${love}가 귀여워요.`);
콜론은 분해하려는 객체의 프로퍼티 : 목표 변수 와 같은 형태로 사용한다.
const person = {
puppy: '미니'
}
// { 객체 프로퍼티: 목표 변수 }
const { name: a = 수민, age = 20, puppy: love } = person;
console.log(`${a}님, ${age}살이시네요!, ${love}가 귀여워요.`);
할당 연산자도 사용할 수 있고, 콜론과 동시에 사용할 수도 있다.
2.4 전개 연산자 (Spread Operator)
예시
let [name, ...rest] = ["Tom", 10, "Seoul"];
console.log(name) // "Tom"
console.log(rest) // [10,"Seoul"]
let names = ["Steve", "John"];
let students = ["Tom", ...names, ...names];
console.log(students) // ["Tom","Steve","John","Steve","John"]
let tom = {
name: "Tom",
age: 10,
region: "Seoul",
};
let steve = {
...tom,
name: "Steve",
};
console.log(steve)
// {
// "name": "Steve",
// "age": 10,
// "region": "Seoul"
// }
key가 동일하면 값이 변경 되는 것을 확인 했다.
2.5 Arrow Functions
예시
const mysum1 = (x, y) => x + y;
const mysum2 = (x, y) => {x, y};
const mysum3 = (x, y) => ({x: x, y: y});
const mysum4 = (x, y) => {
return {x: x, y: y};
}
const mysum5 = function(x, y) {
return {x: x, y: y};
};
function mysum6(x, y) {
return {x: x, y: y};
}
3. 개발환경 세팅
생략
4. CRA(Create React App)
생략
5. React Component
컴포넌트는 React, Vue 등 FE 프레임워크에선 굉장히 중요한 개념이다. 천천히 알아보자.
5.1 React Commponent란 무엇일까?
컴포넌트 개념 이해하기
컴포넌트를 통해 UI를 재사용이 가능한 개별적인 여러 조각으로 나누고, 각 조각을 개별적으로 살펴볼 수 있으며, 개념적으로 컴포넌트는 JS함수와 유사하다.
"props"라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다.
* props는 목차 7에서 다룸
5.2 리액트 컴포넌트를 표현하는 두 가지 방법
함수형 컴포넌트(Function Component)
모든 리액트 컴포넌트는 pure 함수 같은 역할을 해야한다.
리액트의 컴포넌트를 일종의 함수라고 생각하면 된다.
// props 객체를 받아서 엘리먼트를 리턴하는 함수 컴포넌트다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
클래스 컴포넌트(Class Component)
클래스 컴포넌트는 ES6의 Class문법을 사용해서 만들어진 컴포넌트인데, 함수컴포넌트에 비해서 몇 가지 추가적인 기능을 가지고 있다.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
이 컴포넌트는 위에서 봤던 함수 컴포넌트와 동일한 역할을 하는 클래스 컴포넌트다.
함수 컴포넌트와의 가장 큰 차이점은 리액트의 모든 클래스 컴포넌트는 React.Component를 상속 받아서 만든다는 것이다.
상속이라는 것은 객체 지향 프로그래밍에서 나오는 개념이며, 한 클래스의 변수들과 함수들을 상속 받아서 새로운 자식 클래스를 만드는 방법이다.
두가지 모두 기능상으로는 동일하지만, 공홈에서는 함수형 컴포넌트를 사용하기를 권장하고 있다.
6. JSX(JavaScript + XML)
6.1 JSX란?
JavaScript Syntax Extension의 약자로 JS를 확장한 문법이며, React로 개발할 때 사용되므로 공식적인 JS문법은 아니다. 브라우저에서 실행하기 전 바벨이 일반 JS형태로 변환한다.
JSX는 하나의 파일에 JS와 HTML과 같은 마크업을 동시에 작성하여 UI 작업을 편하게 한다.
* JSX에서 사용하는 elements는 DOM요소가 아닌 Virtual DOM요소다.
6.2 주의사항
기초적인 내용이지만 헤멜 수 있는 사항이다.
1. JSX 태그 내부에 JS를 넣는 방법
funtion msg() {
return (
<div>
<h1>{title}</h1>
<p><img src={imgUrl} /></p>
</div>
)
}
중괄호를 사용하며 함수 호출을 포함한 모든 JS 표현식은 중괄호 사이에서 작동한다.
물론 객체도 전달 가능하며 이러한 경우 아래와 같이 이중으로 중괄호를 사용해야 한다.
function TodoList() {
return (
<ul style={{
backgroundColor: 'black',
color: 'pink'
}}>
<li>Improve the videophone</li>
<li>Prepare aeronautics lectures</li>
<li>Work on the alcohol-fuelled engine</li>
</ul>
);
}
* inline style property는 카멜 케이스로 작성
2. 무적권 1개의 엘리먼트를 반환하기
Virtual DOM에서 컴포넌트 변화를 감지할 때 때 효율적으로 비교할 수 있도록 컴포넌트 내부는 하나의 DOM 트리 구조로 이루어져야 한다는 규칙이 있기 때문에 부모요소 하나가 감싸는 형태여야 한다.
function App() {
return (
<p>안녕하세요! 리액트입니다 :)</p>
<div className="App">
<input type='text'/>
</div>
);
}
// parsing error : Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?
위와 같이 작성하면 파싱 에러가 발생하니 욕심 부리지 말고 하나만 리턴해야한다.
return (
<div className="App">
<p>안녕하세요! 리액트입니다 :)</p>
<input type='text'/>
</div>
);
3. class 대신 className
JSX에선 class attribute대신 className을 사용한다.
<div className="App">
7. Props의 개요
7.1 Props란?
먼저 props는 prop 뒤에 복수형을 나타내는 알파벳 s를 붙여서 prop이 여러개인 것을 의미하며, prop은 property라는 단어를 줄여서 쓴것이다.
property는 재산이라는 뜻도 있지만 속성, 특성이라는 뜻도 가지고 있는데, 리액트에서는 컴포넌트의 속성으로 사용된다. 리액트 컴포넌트를 붕어빵틀로 비유 한다면 여기서 props가 나타내는 것은 무엇일까?
props는 붕어빵에 들어가는 재료를 의미한다고 볼 수 있다.
같은 붕어빵이라도 어떤 재료를 넣느냐에 따라 다른 맛이나고 재료가 다른것처럼 props도 컴포넌트에서 눈에 보이는 글자나 색깔 등의 속성을 바꾸는 컴포넌트 속 재료라고 생각하면 된다.
props는 컴포넌트에 전달할 다양한 정보를 자바스크립트 객체로 담고 있다.
컴포넌트에 어떤 데이터를 전달하고 전달된 데이터에 따라 다른 모습의 엘리먼트를 화면에 렌더링 하고 싶을때 props에 넣어서 전달한다.
컴포넌트끼리의 정보교환 방식!
props란 부모 컴포넌트가 자식 컴포넌트에게 물려준 데이터다. 다시 말해, 컴포넌트 간의 정보 교류 방법이다.
아래 사항을 기억하며 예제를 보자.
- props는 반드시 위에서 아래 방향 (부모 => 자식) 방향으로만 흐른다.(단방향)
- props는 반드시 읽기 전용으로 취급하며, 변경하지 않는다.
7.2 props로 값 전달하기
전달 하기 (주체 - 부모)
// src/App.js
import React from "react";
function App() {
return <GrandFather />;
}
function GrandFather() {
return <Mother />;
}
function Mother() {
const name = '홍부인';
return <Child />;
}
function Child() {
return <div>연결 성공</div>;
}
export default App;
여기서 Moter의 name이라는 값을 Child에게 전달하고 싶다면 아래와 같이 props로 전달하면 된다.
// src/App.js
import React from "react";
function App() {
return <GrandFather />;
}
function GrandFather() {
return <Mother />;
}
function Mother() {
const name = '홍부인';
return <Child motherName={name} />; // 💡"props로 name을 전달했다."
}
function Child() {
return <div>연결 성공</div>;
}
export default App;
전달 받기 (주체 - 자식)
import React from "react";
// div안에서 { } 를 쓰고 props.motherName을 넣어보세요.
function Child(props) {
return <div>{props.motherName}</div>;
}
function Mother() {
const name = "홍부인";
return <Child motherName={name} />;
}
function GrandFather() {
return <Mother />;
}
function App() {
return <GrandFather />;
}
export default App;
* props는 객체임
8. Props Children
8.1 children이란?
// src/App.js
import React from "react";
function User() {
return <div></div>;
}
function App() {
return <User>안녕하세요</User>;
}
export default App;
위 코드를 실행하면 아무것도 나오지 않는데, 그 이유는 App에서 "안녕하세요"를 정보를 보냈지만, User에서는 정보를 받지 않았기 때문이다.
부모 컴포넌트에서 정보를 전달 했을때 자식 컴포넌트에선 그 정보를 받아와야 했다.
바로 전에 학습했을때와는 다른 방식으로 props를 전달하고 있는데 이 방식은 children props를 보내는 방식이다.
import React from "react";
function User(props) {
return <div>{props.children}</div>;
}
function App() {
return <User>안녕하세요</User>;
}
export default App;
자식 컴포넌트에서 전달 받는 법은 동일하지만 children을 사용한다.
8.2 children의 용도
Layout 컴포넌트를 만들 때 자주 사용
// src/Header.tsx
const Header = () => {
return <>헤더임</>;
};
export default Header;
// src/Layout.tsx
import { ReactNode } from "preact/compat";
import Header from "./Header";
export const Layout = (props: { children: ReactNode }) => (
<>
<Header />
<div>{props.children}</div>
</>
);
// src/components/About.tsx
import { Layout } from "./components/Layout";
const About = () => {
return (
<Layout>
<>어바웃 컨텐츠임</>
</Layout>
);
};
export default About;
Layout에 있는 header가 보여지게 되고, Layout의 props로 전달된다.
header를 Layout 컴포넌트에서 한번만 작성하면 여러 페이지에서 모두 보여지게 할 수 있다.
8.3 Children 활용
children prop은 어디서 활용이 될지 궁금해졌다. 여러가지 케이스가 있지만 대표적인 것은 렌더링 최적화다.
아래 예시를 보자.
import {useState} from 'react';
const ChildComponent = () =>{
console.log("ChildComponent is rendering!");
return <div>Hello World!</div>
}
const ParentComponent = () =>{
console.log("ParentComponent is rendering!");
const [toggle, setToggle] = useState(false);
return <>
<ChildComponent/>
<button onClick={()=>{setToggle(!toggle)}}>
re-render
</button>
</>
}
const Container =() =>{
return <div>
<ParentComponent/>
</div>
}
Container > ParentComponent > ChildComponent 구조로 프로젝트가 구성되어 있다. 이 상황에서 만약 re-render 버튼을 눌러 ParentComponent의 리렌더링을 유발하면 어떻게 될까?
당연히 ParentComponent가 리렌더 되는 순간 ChildComponent도 함께 리렌더 될 것이다.
만약 이 코드의 성능을 최적화를 해 ParentComponent만 렌더링 되게끔 한다면 아래처럼 React.memo를 사용할 것이다.
import { memo, useState } from "react";
const ChildComponent = memo(() => {
console.log("ChildComponent is rendering!");
return <div>Hello World!</div>;
});
const ParentComponent = () => {
console.log("ParentComponent is rendering!");
const [toggle, setToggle] = useState(false);
return (
<>
<ChildComponent />
<button
onClick={() => {setToggle(!toggle);}}
>
re-render
</button>
</>
);
};
const Container = () => {
return (
<div>
<ParentComponent />
</div>
);
};
export default Container;
하지만 굳이 React.memo를 사용하지 않고도 우리가 방금 학습한 children으로도 렌더링 최적화가 가능하다. 아래를 보자.
import { useState } from "react";
import { ReactNode } from "preact/compat";
const ChildComponent = () => {
console.log("ChildComponent is rendering!");
return <div>Hello World!</div>;
};
const ParentComponent = (props: { children: ReactNode }) => {
console.log("ParentComponent is rendering!");
const [toggle, setToggle] = useState(false);
return (
<div>
{props.children}
<button
onClick={() => {setToggle(!toggle);}}
>
re-render
</button>
</div>
);
};
const Container = () => {
return (
<div>
<ParentComponent>
<ChildComponent />
</ParentComponent>
</div>
);
};
export default Container;
9. Props 추출
9.1 구조분해 할당과 Props
function Todo1({ title }){
return <div>{title}</div>
}
function Todo2({ title, body, isDone, id }){
return <div>{title}</div>
}
props는 object literal이기 때문에 구조분해를 사용하면 귀찮게 props를 붙이지 않아도 된당.
9.2 Default Props
*defaultProps란?
defaultProps란, 부모 컴포넌트에서 props를 보내주지 않아도 설정될 초기 값이다.
// 예시
import React from 'react';
function Child({ name }){
return <div>내 이름은 {name} 입니다. </div>
}
Child.defaultProps={
name: '기본 이름'
}
export default Child
10. State
* State란?
리액트 컴포넌트 내부에서 바뀔 수 있는 데이터 값을 의미한다.
state는 사전에 미리 정해진 것이 아니라 리액트 컴포넌트를 개발하는 각 개발자가 직접 정의해서 사용한다.
*state를 정의할 땐 꼭 렌더링이나 데이터 흐름에 사용되는 것만 state에 포함시켜야 한다. 왜냐하면 state가 변경될 경우 컴포넌트가 재렌더링되기 때문에 렌더링과 데이터 흐름에 관련 없는 값을 포함하면 불필요한 경우 컴포넌트가 다시 렌더링되어 성능을 저하시키기 때문이다.
10.1 State 만들기
state를 만들땐 useState()를 사용한다.
const [ value, setValue ] = useState( 초기값 )
10.2 State 변경하기
state를 변경할때는 setValue()를 사용한다.
// src/App.js
import React, { useState } from "react";
function Child(props) {
return (
<div>
<button
onClick={() => {
props.setName("박할아");
}}
>
할아버지 이름 바꾸기
</button>
<div>{props.grandFatherName}</div>
</div>
);
}
function Mother(props) {
return (
<Child grandFatherName={props.grandFatherName} setName={props.setName} />
);
}
function GrandFather() {
const [name, setName] = useState("김할아");
return <Mother grandFatherName={name} setName={setName} />;
}
function App() {
return <GrandFather />;
}
export default App;
위 코드는 박할아 => 김할아로 바꾸는 코드다. setName을 통해서 바꾼 값은 어디까지나 단순히 화면에서만 바뀌는 것이므로 페이지 리프레시 후엔 다시 돌아간다.
응용 (useState + onChange Event)
input에서는 보통 사용자가 입력한 값을 state로 관리하는 패턴을 사용한다.
import React, { useState } from "react";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (event) => {
const inputValue = event.target.value;
setValue(inputValue);
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={value} />
{value}
</div>
);
};
export default App;
위 코드는 input에 입력한 value를 JS의 event객체에서 value를 꺼내쓰는 방법이다.
위 예시에선 onChange를 사용했는데, 이상하게도 onInput과 같이 동작했다.
JS에선 onInput은 데이터가 바뀔때마다 발생하고 onChange를 사용하면 포커스 아웃 될때 발생하는데?
궁금해서 onInput도 사용해봤지만 onChang와 똑같이 동작했다.
뭔가 다른점이 없는가 궁금해졌다.
const App = () => {
const [inputValue, setInputValue] = useState("");
const [changeValue, setChangeValue] = useState("");
const handleInput = (e) => {
console.log("input");
setInputValue(e.target.value);
};
const handleChange = (e) => {
console.log("change");
setChangeValue(e.target.value);
};
return (
<div>
<input type="text" onInput={handleInput} onChange={handleChange} placeholder="Type with IME" />
<p>onInput Value: {inputValue}</p>
<p>onChange Value: {changeValue}</p>
</div>
);
};
위 코드는 onChange와 onInput에 차이점을 알기 위해 입력을 받을때마다 콘솔에 찍어 보는 코드다.
음.. value의 차이점은 없었고, 실행에서 차이점은 분명히 있었는데, 한글로 같은 글자를 입력하면 onInput은 두번 실행된다.
음,,, 왜 이런지 아시는분은 알려주시면 좋을 것 같다. 넘어가자.,,.
* 왜 이러는지 알아왔음(니다.)
React 공식 문서에서 onChange와 onInput의 설명을 보자.
onChange: An Event handler function. Required for controlled inputs. Fires immediately when the input’s value is changed by the user (for example, it fires on every keystroke). Behaves like the browser input event.
이벤트 처리기 함수입니다. 제어된 입력에 필요합니다. 사용자가 입력 값을 변경하면 즉시 실행됩니다(예: 모든 키 입력 시 실행). 브라우저 입력 이벤트처럼 동작합니다.
onInput: An Event handler function. Fires immediately when the value is changed by the user. For historical reasons, in React it is idiomatic to use onChange instead which works similarly.
이벤트 핸들러 함수입니다. 사용자가 값을 변경하면 즉시 실행됩니다. 역사적 이유로 React에서는 비슷하게 작동하는 onChange를 대신 사용하는 것이 관용적입니다.
onChange는 브라우저 입력 이벤트처럼 동작 하기위해 React의 SyntheticEvent 시스템에 의해 브라우저 input 이벤트를 내부적으로 처리한다.

실제로 native event라는 속성에 접근해보면 change event가 아닌 input event 객체를 가지고 있다.
이는 html 표준과는 다른 의미를 가진다는 것을 알수 있다.
onInput은 브라우저의 input 이벤트와 1:1로 매핑되어 동작한다. IME를 사용할 때 한글과 같은 조합형 문자는 입력 상태가 변할 때마다 연속적으로 input 이벤트를 발생시키는데,
예를 들어 "가"를 입력 할 때
첫 번째 이벤트: ㄱ입력 -> onInput 발생
두 번째 이벤트: ㄱ + ㅏ-> 조합 완료 -> onInput 다시 발생
따라서 IME 입력 과정에서 조합 중간 단계와 완료 단계 각각에 대해 이벤트가 발생한다.
본론으로 들어가서 "ㄱㄱㄱㄱㄱㄴㄴㄴㄴ" 처럼 중복 되는 텍스트를 입력할 땐 왜 콘솔이 두번이나 찍혔을까?
IME 입력기는 입력한 글자가 중간 상태에서 조합 상태로 변환되고, 변환된 값을 최종적으로 입력 필드에 반영한다.
동일한 글자를 반복 입력할 경우도 이 과정이 반복되면서 두 번의 이벤트가 발생하는데,
첫 번째 입력 "ㄱ" : IME가 입력 상태를 감지하고 onInput 이벤트 발생
두 번째 입력 "ㄱ"반복 : 이전 입력을 종료하고 새로운 조합을 시작하면서 outInput 이벤트를 다시 발생.
주요 원인은 리액트에서의 onInput은 브라우저의 네이티브 input 이벤트와 동일하게 동작하기 때문에 IME 입력 중간 상태와 최종 상태를 모두 감지한다. 반복 입력 시에도 IME는 매번 새로운 조합 상태를 트리거하기 때문에 이벤트가 중복으로 실행 되었던 것이다.
정리 하자면
이벤트 | IME 입력 처리 방식 | 중복 이벤트 |
onChange | IME 조합 완료 후 최종 값에 대해서만 이벤트 발생 | 발생하지 않음 |
onInput | IME 조합 상태와 완료 상태 모두 이벤트 발생 | 발생 (조합 중간 + 완료) |
이미 8년전의 누군가는 나와 같은 호기심이 있었다.
개발자의 코멘트를 보면 onChange동작을 JS의 onInput으로 하게 된 결정 당시 브라우저마다 onInput의 동작이 달라 일관성이 없었다고 한다.
다른 플랫 폼에서 웹으로 온 사람들이 onChange에서 "change" 이벤트가 발생할 것으로 예상했기 때문에 onInput 대신 onChange에게 브라우저 무관하게 입력에 변경이 일어날 때마다 이벤트가 트리거 되는 역할을 주었다고 한다.
Controlled input의 경우에는 상태가 입력값으로 바로바로 갱신되는 동작이 필요하며, 이를 위해 관용적으로 onChange가 사용되어져왔기 때문에 리액트에서는 onChange를 사용하는 것이 권장된다고 한다.
JS의 onChange처럼 포커스가 아웃 되었을 때 데이터가 변경 되길 바란다면 onBlur를 사용하라고 한다.
결론
리액트에서 input이 갱신 될 때마다 데이터를 변경하고 싶다면 onInput은 쓰지말고 onChange를 쓰자.
끗
'항해 플러스 > React 스터디' 카테고리의 다른 글
[React 스터디] React Router Dom (0) | 2025.03.04 |
---|---|
[React 스터디] React-Query(Tanstack-Query) (0) | 2025.02.25 |
[React 스터디] Redux (0) | 2025.02.24 |
[React 스터디] 2주차 숙련 단계 (0) | 2025.02.17 |
[React 스터디] 2주차 입문 단계 (1) | 2025.02.16 |