2주차 입문 내용
2025.02.16 - [항해99 플러스/React 스터디] - [React 스터디] 2주차 입문 단계
[React 스터디] 2주차 입문 단계
11. 불변성 & 순수함수11.1 불변성1. 불변성이란?불변성이란 메모리에 있는 값을 변경할 수 없는 것을 말한다. 원시데이터는 불변성이 있고, 원시 데이터가 아닌 객체, 배열, 함수는
s-o-o-min.tistory.com
1. Styled Components - 소개, 사용
리액트에서 CSS-in-JS 방식으로 컴포넌트를 꾸밀 수 있게 도와주는 패키지다.
기본적인 사용 방식은 꾸미고자 하는 컴포넌트를 SC의 방식대로 먼저 만들고, 그 안에 스타일 코드를 작성하는 방식이다.
// src/App.js
import React from "react";
// styled-components에서 styled 라는 키워드를 import 합니다.
import styled from "styled-components";
// styled키워드를 사용해서 styled-components 방식대로 컴포넌트를 만듭니다.
const StBox = styled.div`
// 그리고 이 안에 스타일 코드를 작성합니다. 스타일 코드는 우리가 알고 있는 css와 동일합니다.
width: 100px;
height: 100px;
border: 1px solid red;
margin: 20px;
`;
const App = () => {
// 그리고 우리가 만든 styled-components를 JSX에서 html 태그를 사용하듯이 사용합니다.
return <StBox>박스</StBox>;
};
export default App;
styled. 뒤에 html의 태그를 넣고 백틱을 사용해 css를 넣어주며 사용한다.
props를 통해서 부모에게 값을 전달받고, 조건부 스타일링도 가능하다.
// src/App.js
import React from "react";
import styled from "styled-components";
const StContainer = styled.div`
display: flex;
`;
const StBox = styled.div`
width: 100px;
height: 100px;
border: 1px solid ${(props) => props.borderColor};
margin: 20px;
`;
// 박스의 색을 배열에 담습니다.
const boxList = ["red", "green", "blue"];
// 색을 넣으면, 이름을 반환해주는 함수를 만듭니다.
const getBoxName = (color) => {
switch (color) {
case "red":
return "빨간 박스";
case "green":
return "초록 박스";
case "blue":
return "파란 박스";
default:
return "검정 박스";
}
};
const App = () => {
return (
<StContainer>
{/* map을 이용해서 StBox를 반복하여 화면에 그립니다. */}
{boxList.map((box) => (
<StBox borderColor={box}>{getBoxName(box)}</StBox>
))}
</StContainer>
);
};
export default App;
2. Styled Components - Sass
1.Sass
Sass(Syntactically Awesome Style Sheets)란 css를 효율적으로 사용하기 위해 만들어진 언어이며,
css의 단점을 없애주며, 재사용성을 높이고, 가독성을 높여주기때문에 사용한다.
아래는 Sass 주요 기능이다.
변수 사용
$color: #4287f5;
$borderRadius: 10rem;
div {
background: $color;
border-radius: $borderRadius;
}
Nesting
label {
padding: 3% 0px;
width: 100%;
cursor: pointer;
color: $colorPoint;
&:hover {
color: white;
background-color: $color;
}
}
Import
/* common.scss */
$color1: #ed5b46;
$color2: #f26671;
$color3: #f28585;
$color4: #feac97;
/* ... */
/* style.scss */
@import "common.scss";
.box {
background-color: $color3;
}
3 ~ 7. React Hooks
1. useState
가장 기본적인 hook이며, 함수 컴포넌트에서 가변적인 상태를 가지게 해준다.
const [state, setState] = useState(initialState);
일반적으로 사용하는 방식 말고 함수형 업데이트 방식이 있는데, 그 예시를 보자.
// src/App.js
import { useState } from "react";
const App = () => {
const [number1, setNumber1] = useState(0);
const [number2, setNumber2] = useState(0);
return (
<div>
{/* 버튼을 누르면 1씩 플러스된다. */}
<div>{number1}</div>
<button
onClick={() => {
setNumber1(number1 + 1); // 첫번째 줄
setNumber1(number1 + 1); // 두번쨰 줄
setNumber1(number1 + 1); // 세번째 줄
}}
>
버튼
</button>
{/* 버튼을 누르면 3씩 플러스 된다. */}
<div>{number2}</div>
<button
onClick={() => {
setNumber2((previousState) => previousState + 1);
setNumber2((previousState) => previousState + 1);
setNumber2((previousState) => previousState + 1);
}}
>
버튼
</button>
</div>
);
};
export default App;
버튼 클릭 시 number가 ++ 되는 코드다. 일반적이라면 +1 씩 되겠지만, 함수형으로 사용하게 된다면 한번에 +3이 된다.
일반적인 업데이트 방식은 setNumber가 각각 실행되는 것이 아니라, 배치(batch)로 처리하며, 명령은 세번 내리지만 그 명령을 하나로 모아 최종적으로 한번만 실행을 시킨다.
반면에 함수형 업데이트 방식은 3번을 동시에 명령을 내리면, 그 명령을 모아 순차적으로 각각 1번씩 실행 시켜 전부 더해 +3 이 되는 것이다.
2. useEffect
리액트 컴포넌트가 렌더링 될 때마다 특정 작업을 수행하도록 설정할 수 있는 훅이다.
useEffect는 속한 컴포넌트가 렌더링 될 때마다 실행되기 때문에 의도치 않은 동작을 할 수 있다.
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
useEffect(() => {
console.log("hello useEffect");
});
return (
<div>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</div>
);
}
export default App;
코드를 보면 input에 value를 연결 시켰다. input에 입력을 할 때마다 useEffect가 계속 실행되는 것을 볼 수 있다. 브라우저에 콘솔이 한번만 찍히길 원한다면 의존성 배열을 사용하면 된다.
의존성 배열
의존성 배열이란 이 배열에 값을 넣으면 그값이 바뀔 때만 useEffect를 실행할게 라는 말이다.
// src/App.js
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
useEffect(() => {
console.log("hello useEffect");
}, []); // 비어있는 의존성 배열
return (
<div>
<input
type="text"
value={value}
onChange={(event) => {
setValue(event.target.value);
}}
/>
</div>
);
}
export default App;
의존성 배열에 빈 배열을 넣어 준다면, 처음에 실행될 때 외에는 더 이상 실행 되지 않는다.
3. useRef
다음과 같이 값이 담겨 있으며 current에 접근하여 변경도 가능하다.
state와 다른 점은 컴포넌트가 계속해서 렌더링 되어도 unmount 전까지 값을 유지한다.
그러므로 리렌더링을 발생시키지 않는 값을 저장할 때 유용하다.
import "./App.css";
import { useRef, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
const plusStateCountButtonHandler = () => {
setCount(count + 1);
};
const plusRefCountButtonHandler = () => {
countRef.current++;
};
return (
<>
<div>
state 영역입니다. {count} <br />
<button onClick={plusStateCountButtonHandler}>state 증가</button>
</div>
<div>
ref 영역입니다. {countRef.current} <br />
<button onClick={plusRefCountButtonHandler}>ref 증가</button>
</div>
</>
);
}
export default App;
state는 변경되면 렌더링이 되고, ref는 변경되면 렌더링이 되지 않는다.
import { useEffect, useRef } from "react";
import "./App.css";
function App() {
const idRef = useRef("");
// 렌더링이 될 때
useEffect(() => {
idRef.current.focus();
}, []);
return (
<>
<div>
아이디 : <input type="text" ref={idRef} />
</div>
<div>
비밀번호 : <input type="password" />
</div>
</>
);
}
export default App;
이 코드는 아이디에 포커스를 두는 useRef 예시 코드며, 화면이 처음으로 렌더링 될 때만 ref가 동작하는 것을 볼 수 있다.
4. useContext
리액트에선 일반적으로 부모 컴포넌트 -> 자식 컴포넌트로 데이터를 전달하는데, 만약 컴포넌트가 많아지게 되어 깊이가 깊어진다면 어느 컴포넌트로부터 왔는지 파악이 어려워지게 된다.
이는 어떤 컴포넌트에서 오류가 발생 할 경우 추적이 어려워져 해결이 늦어질 수 밖에 없는데 이것을 해결하고자 나온게 context API다.
context API 필수 개념
- createContext : context 생성
- Consumer : context 변화 감지
- Provider : context 전달 (to 하위 컴포넌트)
// context/ FamilyContext.js
import { createContext } from "react";
export const FamilyContext = createContext(null);
// component/GrandFather.jsx
import React from "react";
import Father from "./Father";
import { FamilyContext } from "../context/FamilyContext";
function GrandFather() {
const houseName = "스파르타";
const pocketMoney = 10000;
return (
<FamilyContext.Provider value={{ houseName, pocketMoney }}>
<Father />
</FamilyContext.Provider>
);
}
export default GrandFather;
// component/Father.jsx
import React from "react";
import Child from "./Child";
function Father() {
return <Child />;
}
export default Father;
// component/ Child.jsx
import React, { useContext } from "react";
import { FamilyContext } from "../context/FamilyContext";
function Child({ houseName, pocketMoney }) {
const stressedWord = {
color: "red",
fontWeight: "900",
};
const data = useContext(FamilyContext);
console.log("data", data)
// {
// houseName: "스파르타",
// pocketMoney: 10000
// }
return (
<div>
나는 이 집안의 막내에요.
<br />
할아버지가 우리 집 이름은 <span style={stressedWord}>{data.houseName}</span>
라고 하셨어요.
<br />
게다가 용돈도 <span style={stressedWord}>{data.pocketMoney}</span>원만큼이나
주셨답니다.
</div>
);
}
export default Child;
GranFather -> Context(중앙 관리소) -> Child 순으로 전달이 되는 모습이다.
Context를 사용할 때, 주의 해야 할 점은 Provider에서 제공한 value가 달라진다면 useContext를 사용하고 있는 모든 컴포넌트가 리렌더링 된다고 한다.
5. 최적화(React.memo, useCallback, useMemo)
시작하기에 앞서
리액트에서는 리렌더링이 빈번하게 일어나기 때문에 비용이 발생하는 것은 최대한 줄여야 한다.
이런 작업을 최적화(optimization)이라 부르며, 불필요한 렌더링이 발생하지 않도록 최적화 하는 대표적인 방법은 아래에 있다.
- memo(React.memo): 컴포넌트를 캐싱
- useCallback: 함수를 캐싱
- useMemo: 값을 캐싱
또한 리렌더링의 발생 조건은 다음과 같다.
- 컴포넌트에서 state가 바뀌었을 때
- 컴포넌트가 내려받은 props가 변경되었을 때
- 부모 컴포넌트가 리렌더링 된 경우 자식 컴포넌트 모두
5.1 memo(React.memo)
1번 컴포넌트가 리렌더링 된 경우, 2 ~ 7번이 모두 리렌더링 되고 4번 컴포넌트가 리렌더링 된 경우 6, 7번이 모두 리렌더링 되는데, 이는 매우 비효율적일 수 도 있다.
아래 예제를 살펴보겠다.
// App.jsx
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";
const boxesStyle = {
display: "flex",
marginTop: "10px",
};
function App() {
console.log("App 컴포넌트가 렌더링되었습니다!");
const [count, setCount] = useState(0);
// 1을 증가시키는 함수
const onPlusButtonClickHandler = () => {
setCount(count + 1);
};
// 1을 감소시키는 함수
const onMinusButtonClickHandler = () => {
setCount(count - 1);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 />
<Box2 />
<Box3 />
</div>
</>
);
}
export default App;
// Box1.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#91c49f",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box1() {
console.log("Box1이 렌더링되었습니다.");
return <div style={boxStyle}>Box1</div>;
}
export default Box1;
// Box2.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#4e93ed",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box2() {
console.log("Box2가 렌더링되었습니다.");
return <div style={boxStyle}>Box2</div>;
}
export default Box2;
// Box3.jsx
import React from "react";
const boxStyle = {
width: "100px",
height: "100px",
backgroundColor: "#c491be",
color: "white",
// 가운데 정렬 3종세트
display: "flex",
justifyContent: "center",
alignItems: "center",
};
function Box3() {
console.log("Box3가 렌더링되었습니다.");
return <div style={boxStyle}>Box3</div>;
}
export default Box3;
App.jsx에서 count를 변경하여 리렌더링이 발생한다면, 자식들도 같이 리렌더링이 되어 아주 비효율적인 상황이 발생한다.
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);
위와 같이 React.memo()를 사용하게 되면 state의 변경으로 인해 props가 변경되지 않는 한 자식은 리렌더링 되지 않는데,
이를 컴포넌트 memoiztion이라고 한다.
5.2 useCallback
React.memo는 컴포넌트를 메모제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 메모제이션 한다.
Box1에서 count를 초기화하는 함수를 작성해보자.
// App.jsx
...
// count를 초기화해주는 함수
const initCount = () => {
setCount(0);
};
return (
<>
<h3>카운트 예제입니다!</h3>
<p>현재 카운트 : {count}</p>
<button onClick={onPlusButtonClickHandler}>+</button>
<button onClick={onMinusButtonClickHandler}>-</button>
<div style={boxesStyle}>
<Box1 initCount={initCount} />
<Box2 />
<Box3 />
</div>
</>
);
}
...
// Box1.jsx
...
function Box1({ initCount }) {
console.log("Box1이 렌더링되었습니다.");
const onInitButtonClickHandler = () => {
initCount();
};
return (
<div style={boxStyle}>
<button onClick={onInitButtonClickHandler}>초기화</button>
</div>
);
}
...
React.memo를 사용했지만 count를 수정하거나 초기화 할때 Box1도 같이 리렌더링 되는것을 볼수 있다.
JS에서는 함수도 객체의 한 종류기 때문에 다시 만들어지면 주소값이 달라지고, 이에 따라 하위컴포넌트는 props가 변경 되었다고 인식하게 되어 리렌더링이 된다.
이 또한 매우 비효율 적일 수 있으므로 useCallback을 사용하여 수정해보자.
// App.js
// 변경 전
const initCount = () => {
setCount(0);
};
// 변경 후
const initCount = useCallback(() => {
setCount(0);
}, []);
수정 후엔 Box1.jsx는 리렌더링이 되지 않지만, useCallback이 count가 0일 때 메모리에 함수를 저장 했기 때문에 count가 0으로 나온다.
이럴 땐 의존성 배열에 count를 추가하면 된다.
...
// count를 초기화해주는 함수
const initCount = useCallback(() => {
console.log(`[COUNT 변경] ${count}에서 0으로 변경되었습니다.`);
setCount(0);
}, [count]);
...
이렇게 하면 count도 잘 저장 된다.
5.3 useMemo
동일한 값을 반환하는 함수를 계속 호출하면 매우 비효율적인 렌더링이라고 볼 수 있다.
맨 처음 해당 값을 반환할 때 그 값을 메모리에 저장하는데, 이렇게 하면 필요할 때마다 다시 함수를 호출해서 계산하는것이 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있다. 이러한 기법을 캐싱을 한다.라고 표현을 한다.
// as-is
const value = 반환할_함수();
// to-be
const value = useMemo(()=> {
return 반환할_함수()
}, [dependencyArray]);
useMemo는 이러한 형식으로 사용되며 아래 예시를 보며 좀 더 자세히 확인해 보자.
// App.jsx
import "./App.css";
import HeavyComponent from "./components/HeavyComponent";
function App() {
const navStyleObj = {
backgroundColor: "yellow",
marginBottom: "30px",
};
const footerStyleObj = {
backgroundColor: "green",
marginTop: "30px",
};
return (
<>
<nav style={navStyleObj}>네비게이션 바</nav>
<HeavyComponent />
<footer style={footerStyleObj}>푸터 영역이에요</footer>
</>
);
}
export default App;
// HeavyComponent.jsx
import React, { useState, useMemo } from "react";
function HeavyButton() {
const [count, setCount] = useState(0);
const heavyWork = () => {
for (let i = 0; i < 1000000000; i++) {}
return 100;
};
// CASE 1 : useMemo를 사용하지 않았을 때
const value = heavyWork();
// CASE 2 : useMemo를 사용했을 때
// const value = useMemo(() => heavyWork(), []);
return (
<>
<p>나는 {value}을 가져오는 엄청 무거운 작업을 하는 컴포넌트야!</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
누르면 아래 count가 올라가요!
</button>
<br />
{count}
</>
);
}
export default HeavyButton;
위는 굉장히 무거운 count 컴포넌트다. count를 올리려면 굉장히 많은 시간이 소모되는데, 이는 useMemo를 사용한다면
많은 시간이 들지 않는다.
useMemo를 남발하게 되면 별도의 메모리 확보를 너무나 많이하여 오히려 성능이 악화될 수 있다.
8. LifeCyle - 클래스형 컴포넌트
리액트 컴포넌트는 Mount -> Update -> Unmount의 과정을 거친다.
8.1 Mount
1. constructor
- 컴포넌트가 맨 처음 만들어 질 때 호출
- 생성자
2. getDerivedStateFromProps
- 부모 컴포넌트로부터 props를 전달받을 때, state에 값을 일치시키는 역할을 하는 메서드
- 마운트 될 때, 업데이트(리렌더링) 될 때도 호출
3. render
- 최초 mount가 준비완료 되면 호출되는, 즉 렌더링 하는 메서드
- 컴포넌트를 DOM에 마운트하기 위해 사용
4. componentDidMount
- 컴포넌트가 브라우저에 표시가 된 후 호출되는 메서드
8.2 Update
1. getDerivedStateFromProps
- Mount 과정에서도 동일하게 호출되었던 메서드
- 부모 컴포넌트로부터 props를 전달받을 때, state에 값을 일치시키는 메서드
2. shouldComponentUpdate
- 리렌더링 여부 판단(boolean) true = 리렌더링 O, false = 리렌더링 X
- 함수형 컴포넌트에서 memo, useMemo, useCallback이 역할을 대신한다.
3. render
- 변경사항 반영이 다 되어 준비완료 되면 호출되는, 즉 렌더링 하는 메서드
- 컴포넌트를 DOM에 마운트하기 위해 사용
4. getSnapshotBeforeUpdate
- 컴포넌트에 변화가 일어나기 직전 DOM의 상태를 저장
- componentDidUpate 함수에서 사용하기 위한 스냅샷 형태의 데이터
5. componentUpdate
- 컴포넌트 업데이트 작업 완료 후 호출
8.3 Unmount
1. componentWillUnmount
- 컴포넌트가 사라지기 전 호출되는 메서드
- useEffect의 return과 동일
9. DOM과 Virtual DOM
9.1 DOM
DOM(Document Object Model)은 웹 문서의 구조화된 표현으로, HTML요소를 JS Object처럼 조작할 수 있는 Model이다.
DOM은 트리 자료 구조로 구축되며, 트리 자료 구조는 하나의 최상위 노드에서 다른 자식 노드들이 뻗어나가는 구조다.
아래 예시와 같이 document 노드가 맨 위에 있고 이어서, element, text, attribute 노드가 나오는 계층적 구조가 있는 것을 볼 수 있다.
DOM이 생성되는 순서
HTML 웹 페이지는 HTML 파서에 의해 DOM으로 변환되는데, 중간에 <script> 태그를 만나면 파서는 DOM 생성을 중지하고 JS엔진이 script에 정의된 파일 및 코드를 실행한다.
이 후 스크립트 실행이 완료되면 다시 HTML파서가 DOM을 생성한다.
즉, 브라우저는 동기적으로 HTML과 JS를 처리하기 때문에 <script> 태그의 위치에 따라 DOM 생성이 느려질 수 있다.
그렇기 때문에 <script>태그는 HTML 문서 맨 하단에 넣는것을 추천한다. DOM 생성이 지연되지 않고, 스크립트에 DOM을 접근하는 코드가 있다면 제대로 실행되지 않을 것이기 때문이다.
9.2 가상 DOM(Virtual DOM)
말 그대로 가상으로 만들어지는 DOM이며 실제 DOM과 구조가 완벽히 동일한 복사본 형태다.
Object 형태로 메모리에 저장되기 때문에 실제 DOM을 조작하는 것 보다 훨씬 더 빠르게 조작을 수행할 수 있다.
리액트는 두개의 가상돔 객체를 가지고 있다.
1. 렌더링 이전 화면 구조를 나타내는 가상 돔
2. 렌더링 이후에 보이게 될 화면 구조를 나타내는 가상돔
리렌더링이 발생 할 떄마다 새로운 내용이 담긴 가상돔을 생성하게되는데, 이는 실제 브라우저가 그려지기 이전에 생성한다.
렌더링 이전에 화면의 내용을 담고있는 첫번째 가상돔과 업데이트 이후에 발생할 두번째 가상돔을 어떤 element가 변했는지 비교하는데, 이를 Diffing이라 표현한다.
Diffing을 통해 차이가 발생한 부분만(브라우저 상의) 실제 DOM에 적용하게 되는 것이다.
이 과정을 Reconciliation(재조정)이라고 한다.
'항해99 플러스 > 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주차 입문 단계 (1) | 2025.02.16 |
[React 스터디] 1주차 입문 단계 (1) | 2025.02.05 |