왜 리액트를 사용하는가
- JS를 사용하여 이벤트 핸들링을 하려면 id를 사용하여 각 DOM을 선택한 뒤, 원하는 이벤트가 발생하면 DOM의 특정 속성을 바꾸어주어야 한다.
- 사용자와의 인터랙션이 별로 없는 웹페이지면 상관없을 수 있지만, 만약에 인터랙션이 자주 발생하고, 이에 따라 동적으로 UI를 표현해야 한다면 이러한 규칙이 정말 다양해질 것이고, 그러면 관리하기도 힘들어질 것이다.
- 그리고 대부분의 경우 웹 애플리케이션의 규모가 커지면, DOM을 직접 건드리면서 작업을 할 시 코드가 난잡해지기 쉽다.
- Ember, Backbone, AngularJS 등의 프레임워크가 만들어졌었는데, 작동방식은 각각 다르지만 쉽게 설명하면 JS의 특정 값이 바뀌면 특정 DOM의 속성이 바뀌도록 연결을 해주어 업데이트 작업을 간소화 해주는 방식이다.
그러나 리액트는 다 날리고 새로 만들어서 보여준다는 아이디어에서 개발이 시작되었다.
속도 문제는 Virtual DOM을 사용하여 해결했다.
Virtual DOM ( 가상 DOM )
UI를 자바스크립트 객체 형태의 값으로 표현한 것이다.
위에서 말한 <h1> 같은 요소 하나하나가 Virtual DOM 자체인 것은 아니고, Virtual DOM을 구성하는 노드라고 볼 수 있다!
Virtual DOM의 구조
React에서는 전체 UI를 Virtual DOM 트리 ( 객체의 계층 구조 )로 표현한다.
즉, Virtual DOM은 실제 DOM을 추상화한 JS 객체들의 트리 구조라고 할 수 있다.
UI를 자바스크립트 객체로 표현하기 위해 React.createElement의 연쇄적인 호출이 일어난다.
React Application의 첫번째 렌더링 동안에는 Virtual DOM과 Real DOM 트리가 모두 생성된다.
컴포넌트의 상태를 업데이트하는 함수를 호출하면 업데이트가 필요하다는 표시를 한다. (Dirty Check)
Dirty Checking
Dirty Checking은 데이터가 변경되었는지 감지하는 방식 중 하나다.
이전 값이 현재 값 비교(diff) 해서, 변경이 있으면 업데이트하는 방식이다.
- 렌더링 주기마다 전체 데이터를 훑음
- 이전 값과 현재 값 비교(diffing)
- 값이 다르면 변경이 된 것으로 판단하고 업데이트
- 데이터가 최신 상태인지 검사를 위해 재귀적으로 순회해야 한다
더티체크 방식은, 전체 데이터를 일일이 확인하여 변경을 감지한다. → 성능 저하
리액트는 Virtual DOM을 이용해 변경된 부분만 찾아서 업데이트한다. → 성능 최적화
변경 사항을 “최소한의 연산”으로 찾아서 반영한다.
✅ React Virtual DOM도 Dirty Check 개념을 사용한다!
개념이 적용되는 방식
- 최초 렌더링 → Virtual DOM과 Real DOM이 함께 생성된다.
- 상태 업데이트가 발생 → setState 호출
- React는 변경된 컴포넌트를 (Dirty)로 표시
- Virtual DOM을 다시 만들고 이전 Virtual DOM과 비교(Diffing) → 변경된 부분만 업데이트
- 동작 방식컴포넌트 단위로 트리를 재생성한다. → setState가 호출되면 state를 사용하는 컴포넌트만 다시 Virtual DOM을 생성한다. = 불필요한 부분은 건드리지 않음원래 트리와 비교, 변경된 부분만 Real DOM에 적용 - Real DOM에는 최소한의 변경만 반영하기 때문에 성능이 최적화 됨
- 예를 들어 setState가 호출되면 state를 사용하는 노드의 부분만 변경된 트리 생성,
- Virtual DOM을 새로 생성하는 게 비효율적인 것 같지만,
리액트 실행해보기
리액트 컴포넌트를 만들 땐,
import React from 'react';
이렇게 리액트를 불러와주어야 한다.
또한 리액트 컴포넌트에서는 XML 형식의 값을 반환해줄 수 있는데, 이를 JSX라고 부른다.
export default Hello; // 컴포넌트를 내보내겠다는 의미
index.js 파일을 보면, App 컴포넌트를 받아와서 실제 DOM 내부에 렌더링하는데(ReactDOM.render), 여기서 id가 root인 DOM을 선택했다.
id가 root인 DOM은 index.html에 있는데,
<div id="root"></div>
리액트 컴포넌트가 렌더링 될 때에는, 렌더링된 결과물이 위 div 내부에 렌더링되는 것이다.
createRoot란 무엇인가
강의를 보다가, 설명과 다른 부분이 나와서 찾아보았다.
createRoot로 브라우저 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성할 수 있다.
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(<App />);
React는 domNode에 대한 루트를 생성하고 그 안에 있는 DOM을 관리한다. 루트를 생성한 후에는 root.render를 호출하여 그 안에 React 컴포넌트를 표시해야 한다.
여기서 domNode가 id가 root인 엘리먼트이다.
대충…… 이정도
JSX
얼핏보면 HTML 같지만, 실제로는 JS다.!.!.!.!.!
엄 BABEL 저거 어떻게 쓰는지 몰라서 직접 코드치겠습니다.
<div>
<b>Hello</b>
</div>
"use strict";
React.createElement("div", null, React.createElement("b", null, "Hello"));
// 대충 이런 식으로 계속 만들어줌
지켜야 하는 규칙
- 태그는 꼭 닫혀있어야 한다. (<div> → 이렇게만 있으면 안됨) 열었으면 닫으라 했다.
- 만약 혼자쓰는거면 Self Closing 태그 사용해야함 ( <br /> )
- img 태그나 a 태그 쓸 때 많이 씀, 컴포넌트에도 많이 써요
- 두 개 이상의 태그는 무조건 하나의 태그로 감싸져 있어야 한다.
- 왜 그래야 하는 것인가 ⇒ JSX 문법은 JS의 React.createElement() 함수로 변환되는데, 이 함수는 하나의 루트 요소만 반환할 수 있다.
- 그래서 태그로 감싸주거나, Fragment(빈 태그)를 사용하면 된다.
- 변수를 보여주어야 할 때는 { }로 감싸서 보여준다.
- <div>{name}</div>
- JS에서 주석은 {/* */} 이런 형태로 작성한다. ( 중괄호 없으면 화면에 보인다 )
props를 통해 컴포넌트에게 값 전달하기
props는 properties의 줄임말로, 컴포넌트에게 어떤 값을 전달해줘야 할 때 사용한다.
예를 들어 App 컴포넌트에서 Hello 컴포넌트를 사용할 때 name이라는 값을 전달해주고 싶다면
// App.js
<Hello name="react" />
// Hello.js
<div>안녕하세요 {props.name}</div>
이렇게 하면 된다. props는 객체 형태로 전달되며, name값을 조회하고 싶으면 props.name을 조회하면 된다.
비구조화 할당을 사용하면 여러개의 props를 받았을 때 코드를 더 간결하게 작성할 수 있다.
import React from "react";
function Hello({ color, name }) {
return <div style={{ color }}>안녕하세요 {name} 기초 시작합니다.</div>;
}
export default Hello;
컴포넌트에 props를 지정하지 않았을 때 기본적으로 사용할 값을 설정하고 싶다면 컴포넌트에 defaultProps라는 값을 설정하면 된다. → 근데 뭐 찾아보니까 지원 중단 예정이라 하네요
Hello.defaultProps = {
name='이름없음'
}
// 이렇게 쓰면 됨
function Hello({color, name='이름없음'}){
return <div style={{ color }}>안녕하세요 {name} 기초 시작합니다.</div>;
}
props.children
컴포넌트 태그 사이에 넣은 값을 조회하고 싶을 땐, props.children 조회하면 된다.
Wrapper 컴포넌트를 만들어서 Hello 컴포넌트를 감쌌을 때,
function Wrapper() {
const style = {
border: '2px solid black',
padding: '16px',
};
return (
<div style={style}>
</div>
)
}
이러면 아무것도 보이지 않는다. - 내부 값이 보이게 하려면 props.children을 렌더링해주어야 한다.
조건부 렌더링
조건부 렌더링이란, 특정 조건에 따라 다른 결과물을 렌더링 하는 것을 의미한다.
일반적으로 삼항연산자를 사용하는데,
function Hello({ color, name, isSpecial }) {
return (
<div style={{ color }}>
{ isSpecial ? <b>*</b> : null }
안녕하세요 {name}
</div>
);
}
여기서 isSpecial 값이 true면 <b>*</b>를, 그렇지 않으면 null을 보여주도록 하였다. 참고로 JSX에서 null, false, undefined를 렌더링하게 되면 아무것도 나타나지 않게 된다.
보통 삼항연산자를 사용하는 건 특정 조건에 따라 보여줘야 하는 내용이 다를 때 사용한다.
지금은 내용이 달라지는 것이 아니라, 단순히 특정 조건이 true면 보여주고 그렇지 않으면 숨겨주고 있는 거라서 이러한 상황에서는 && 연산자를 사용해서 처리하는 것이 간편하다.
{isSpecial && <b>*</b>}
또한 props 값 설정을 생략하면 true로 설정한 것으로 간주한다.
isSpecial == isSpecial = {true}
useState → 컴포넌트에서 바뀌는 값 관리하기
동적인 부분을 담당한다.
import React, { useState } from "react";
function Counter() {
const [number, setNumber] = useState(0);
const onIncrease = () => {
setNumber(number + 1);
};
const onDecrease = () => {
setNumber(number - 1);
};
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
}
export default Counter;
이렇게 useState를 사용하여 동적으로 값을 변경할 수 있다.
- useState 함수는 배열을 반환하는데, 첫번째 원소는 현재 상태, 두 번째 원소는 Setter 함수를 반환한다.
- 여기서 비구조화 할당을 통하여 각 원소를 추출해서 [number, setNumber] 이렇게 추출해준 것이다.
🚨주의할 점
onclick = {onIncrease()} // X -> 이건 함수를 실행하는 것임
이벤트를 설정할 때에는 함수타입의 값을 넣어주어야 한다.
함수형 업데이트
지금은 setNumber와 같은 Setter 함수를 사용할 때, 업데이트 하고 싶은 값을 파라미터로 넣어주고 있는데, 그 대신 기존 값을 어떻게 업데이트 할 지에 대한 함수를 등록하는 방식으로도 값을 업데이트 할 수 있습니다.
const onIncrease = () => {
setNumber((prev) => prev + 1);
};
const onDecrease = () => {
setNumber((prev) => prev - 1);
};
prev ⇒ prev + 1 은 콜백 함수이다.
업데이트 함수라고도 부르는데, useState에서 setState에 함수를 넘기면, 그 함수의 인자는 현재 상태 값(prev) 이다.
“현재 number 값 좀 가져와서 +1 / -1 해라”
이렇게 호출한다고 생각하면 된다.
왜 쓸까?
값을 바로 계산해도 되지만, 이전 상태에 의존할 때는 콜백 방식이 더 안전하다.
setNumber(number + 1); // 동시 호출 시 꼬일 수 있다.
setNumber((prev) => prev + 1); // 이전 상태 기반으로 안전
input 상태 관리
import React, { useState } from "react";
function InputSample() {
const [text, setText] = useState("");
const onChange = (e) => {
setText(e.target.value);
};
const onReset = (e) => {
setText("");
e.target.value = ;
};
return (
<div>
<input onChange={onChange} />
<button onClick={onReset}>초기화</button>
<div>
<b>값: {text}</b>
</div>
</div>
);
}
export default InputSample;
여기서 onChange라는 이벤트를 사용하는데, 이벤트에 등록하는 함수에서는 이벤트 객체 e를 파라미터로 받아와서 사용할 수 있는데, e.target은 이벤트가 발생한 DOM인 input DOM을 가리키게 됩니다.
이 DOM 의 value 값, 즉 e.target.value를 조회하면 현재 input 에 입력한 값이 무엇인지 알 수 있다.
input 여러 개 관리하기
단순히 useState를 여러번 사용하고, onChange도 여러개 만들어서 구현할 수 있지만, 가장 좋은 방법은 아니다.
더 좋은 방법은, input에 name을 설정하고 이벤트가 발생했을 때 이 값을 참조하는 것이다. useState에서는 문자열이 아니라 객체 형태의 상태를 관리해주어야 한다.
import React, { useState } from "react";
function InputSample() {
const [inputs, setInputs] = useState({
name: "",
nickname: "",
});
const { name, nickname } = inputs;
const onChange = (e) => {
const { name, value } = e.target;
setInputs({
...inputs,
[name]: value,
});
};
const onReset = (e) => {
setInputs({
name: "",
nickname: "",
});
};
return (
<div>
<input name="name" placeholder="이름" onChange={onChange} value={name} />
<input
name="nickname"
placeholder="닉네임"
onChange={onChange}
value={nickname}
/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} ({nickname})
</div>
</div>
);
}
export default InputSample;
리액트에서 객체를 수정해야 할 때에는,
inputs[name] = value;
이런 식으로 직접 수정하면 안되고, 새로운 객체를 만들어서 새로운 객체에 변화를 주고, 이를 상태로 사용해주어야 한다.
setInputs({
...inputs,
[name]: value,
});
이러한 작업을 불변성을 지킨다고 한다.
불변성을 지켜야 하는 이유가 무엇인가?
React의 상태 변경 감지 원리 때문
React는 상태의 변화를 감지해야 컴포넌트를 다시 렌더링할 수 있다.
React는 상태가 변경되었는지 확인할 때 객체의 메모리 주소(참조)를 비교한다.
const [count, setCount] = useState({value:0});
const increase = () => {
count.value += 1;
setCount(count);
};
count.value += 1을 하면, 기존 객체 자체가 변경되지만 메모리 주소는 그대로이므로 React가 상태 변경을 감지하지 못한다.
setCount(count)를 호출해도 React는 상태가 변했다고 인식하지 않아서 렌더링되지 않음.
올바른 예시 (새로운 객체 생성)
const increase = () => {
setCount((prev) => ({ value: prev.value + 1 })); // ✅ 새로운 객체를 생성
};
{ value: prev.value + 1 }처럼 새로운 객체를 만들어서 setState에 전달해야 React가 변경을 감지하고 렌더링한다.
→ 그럼 메모리 주소가 바뀌는 것인가? 그럼 원래 있던 메모리 주소는 어떻게 되는가
→ 새로운 객체는 새로운 메모리 주소를 가짐. → 변경이 발생했다고 판단, 컴포넌트 리렌더링
→ 이전 객체의 메모리는 해제되냐? → JS의 가비지 컬렉터가 처리한다.
이전 객체가 더 이상 사용되지 않으면 자동으로 메모리에서 제거된다.(메모리 해제)
React의 state는 필요할 때만 메모리를 사용하고, 필요없으면 자동으로 정리된다는 개념
과제 - 자기소개 카드 만들기
import React, { useState } from "react";
import styled from "styled-components";
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
`;
const CardIntro = styled.div`
&:hover {
transform: translateY(-10px);
transition-duration: 0.5s;
}
transition-duration: 0.5s;
`;
const Intro = styled.div`
background-color: black;
color: white;
border: 1.5px solid gray;
border-radius: 10px;
width: 150px;
height: 250px;
padding: 12px;
`;
const Button = styled.button`
border: 0.5px solid gray;
border-radius: 5px;
background-color: black;
color: white;
`;
function Card({ name, age, hobby }) {
const [food, setFood] = useState("");
const [isChange, setChange] = useState(false);
const onClick = () => {
setChange(!isChange);
};
const onChange = (e) => {
setFood(e.target.value);
};
return (
<Container>
<h1>자기소개 카드</h1>
<CardIntro>
<Intro>
이름 : {name}
<p />
나이 : {age}
<p />
취미 : {hobby}
<p />
<span>
좋아하는 음식 : <br />
{isChange ? (
<input
style={{ width: "80%" }}
onChange={onChange}
placeholder="뭐 먹을래"
/>
) : (
food
)}
</span>
<p />
<Button onClick={onClick}>변경</Button>
</Intro>
</CardIntro>
</Container>
);
}
export default Card;
검색을 막 해보다가 styled component를 사용하면 쉽게 할 수 있다고 해서 한 번 사용해봤다.
장점은
css-in-js는 JS 환경을 최대한 활용할 수 있다.
또한 JS와 CSS 사이의 상수와 함수를 쉽게 공유할 수 있다.
어쨌든 사용해서 스타일 부분을 만들었습니다.
커서를 카드에 올리면 올라갔다 내려갔다 한답니다
변경 버튼 누르면 이렇게 나오고 변경하면 좋아하는 음식 바뀌게 만들었습니다
끝
'WINK-(Web & App) > React.js 스터디' 카테고리의 다른 글
[2025 1학기 React.js 스터디] 강민지 #3주차 (0) | 2025.04.09 |
---|---|
[2025 1학기 React.js 스터디] 이승준 #3주차 (0) | 2025.04.08 |
[2025 1학기 React.js 스터디] 이상래 #3주차 (0) | 2025.04.06 |
[2025 1학기 React.js 스터디] 이종민 #2주차 (0) | 2025.04.04 |
[2025 1학기 React.js 스터디] 백채린 #2주차 (0) | 2025.04.04 |