useFormInput을 두개를 호출했는데 이럴 경우 state가 1개로 공유하는 게 아니라 각각 들어가게 됩니다. state를 공유하는 게 아닌 state 저장 로직을 공유함으로 독립적이다.
커스텀 Hook 안의 코드는 컴포넌트가 재렌더링될 때마다 다시 돌아갈 겁니다. 이게 바로 커스컴 Hook이 (컴포넌트처럼) 순수해야하는 이유 입니다.
import { useState } from 'react';
export default function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel
title="About"
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
</Panel>
<Panel
title="Etymology"
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
</Panel>
</>
);
}
function Panel({
title,
children,
isActive,
onShow
}) {
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
Show
</button>
)}
</section>
);
}
만약 state를 공유해야하면 위 예제 처럼 부모 state를 정의하고 그 state를 props로 전달하면 된다.
또다른 예제
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// 이 Effect는 나라별 도시를 불러옵니다.
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// 이 Effect 선택된 도시의 구역을 불러옵니다.
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);
// ...
/** ──── custom hook으로 변환 ────**/
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}
useEffect은 어떤 값이 변화되었다는 걸 감지해서 감지했을 때 안에 있는 익명함수를 실행시킵니다
useEffect 사용 방법
useEffect(() => {
// 모든 렌더링 후에 실행됩니다
});
useEffect(() => {
// 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);
useId 변경 → 언마운트로 ignore true 동작 → DataFetch → setData로 리렌더링
fetch에서 clean-up 작업을 한 이유는 예를 들면 네트워크가 안 좋은데select box를 엄청나게 바꾸면네트워크 요청은 여러개가 나가고 리렌더링작업은 큐처럼 쌓이게 되어 뒤늦게 {data}가 큐에 쌓인대로 API요청한 회수만큼 와따가따하게 된다 clean-up 작업을 한 경우는 언마운트시 작동함으로 전 네트워크 요청에 대해서 ignore true값을 줘서 리렌더링 안 하게 한다
동작 과정은 아래와 같다
Select box 변경 → 1번 네트워크 요청 → 1번 네트워크 요청중에 Select box 또 변경 → 1번 네트워크 요청 결과를 await중에 clean-up 내용 작동으로 ignore가 true로 변경되어서 데이터 받아와도 setData 작동 안 해서 변경 없음 → 2번 네트워크 요청 await후 input의 변경이 없기 때문에 ignore은 false상태이기 때문에 리렌더링이 된다
useEffect 사용할 때 주의할 점이 있다
useEffect의 경우 렌더링 된 이후에 작동하기 때문에 렌더링 전에 작동시켜야하면 서버사이드에서 해결해줘야한다
useEffect만 사용해서 데이터를 fetch하는 경우 미리 데이터를 로드하거나 캐시하지 않기 때문에 느릴 수도 있다
위와 같은 문제점을 해결하기 위해서는 일반적으로 프레임워크에 있는 내장 데이터 패칭기능이나 오픈 소스 솔루션인 React Query, useSWR를 사용하면 네트워크 응답을 캐싱하며 네트워크를 기본적으로 최적화하니 사용하는게 좋다
useEffect 잘 사용하기
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 피하세요: 중복된 state 및 불필요한 효과
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos()가 느리지 않다면 괜찮습니다.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ todos 또는 filter가 변경되지 않는 한 다시 실행되지 않습니다.
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
props의 todos랑 filter랑 변경되면서 다시 해당 함수인 TodoList를 실행하기 때문에 굳이 state로 관리할필요 없고 필요사항에만 메모이제이션해서 계산하게 하면 더욱 좋다
어떤 경우 메모이제이션을 해야할까?
전체적으로 기록된 시간이 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모이제이션하는 것이 좋습니다가장 정확한 시간을 얻으려면 프로덕션용 앱을 빌드하고 사용자가 사용하는 것과 같은 기기에서 테스트해야합니다
function App() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 위 useState 값이 변경되면서 전체를 실행하기 때문에 state로 상태관리를 할 필요도 없다
const fullName = firstName + ' ' + lastName;
// 🔴 피하세요: 중복된 state 및 불필요한 Effect
//const [fullName, setFullName] = useState('');
//useEffect(() => {
// setFullName(firstName + ' ' + lastName);
//}, [firstName, lastName]);
return (
<div>
<input type="text" onChange={(e)=>setFirstName(e.target.value)} />
<input type="text" onChange={(e)=>setLastName(e.target.value)} />
{fullName}
</div>
);
}
입력따위로 set하는 경우가 따로 있지 않는 경우는 state로 만들 필요가 없다
// ProfilePage 컴포넌트: userId를 prop으로 받아서 Profile 컴포넌트에 전달
function ProfilePage({ userId }: {userId:string}) {
return (
<Profile
userId={userId}
key={userId} // userId가 변경될 때마다 새로운 Profile 컴포넌트가 마운트됨
/>
);
}
// Profile 컴포넌트: userId를 prop으로 받아서 사용
function Profile({ userId }:{userId:string}) {
const [comment, setComment] = useState('');
return (
<div>
<h1>Profile for user: {userId}</h1>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Leave a comment"
/>
</div>
);
}
function App() {
const [userId, setUserId] = useState('user1');
return (
<div>
<button onClick={() => setUserId('user1')}>User 1</button>
<button onClick={() => setUserId('user2')}>User 2</button>
<ProfilePage userId={userId} />
</div>
);
}
userId값이 바뀌어서 props값이 달라지면서 리렌더링이 되는데 일반적으로 React는 동일한 컴포넌트가 같은 위치에 렌더링 될 때 state를 보존하기 때문에 comment값은 초기화가 안 됩니다 그렇기 때문에 key를 이용해 서로 다른 컴포넌트라는 걸 인식시켜줘야합니다
key 설정을 따로 못하는 경우 같은 위치 컴포넌트의 state 초기화 시켜야할 때는 위와 같은 방법을 이용할 수 있습니다
/** bad code **/
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 피하세요: 서로를 트리거하기 위해서만 state를 조정하는 Effect 체인
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ...
/** right code **/
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ 렌더링 중에 가능한 것을 계산합니다.
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ 이벤트 핸들러에서 다음 state를 모두 계산합니다.
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
웬만해서 Effect를 남발하지말고 Effect 없이 해결할 수 있는지 파악한다 그리고 같은 맥락의 있는 경우 Effect를 하나로 묶어주는게 더 좋다 위 예시는 여러 개의 useEffect 훅을 사용하여 상태 업데이트를 체이닝하면 비효율적이고 유지보수가 어려운 코드를 만들 수 있다는 것을 보여줍니다
import React from 'react';
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}
return (
<div>
<button onClick={handleClick}>{isOn ? 'ON' : 'OFF'}</button>
<div
onDragEnd={handleDragEnd}
style={{ width: '100px', height: '100px', backgroundColor: 'lightgray' }}
/>
</div>
);
}
export default Toggle;
import React, { useState } from 'react';
import Toggle from './Toggle';
function App() {
const [isOn, setIsOn] = useState(false);
function handleToggleChange(nextIsOn) {
setIsOn(nextIsOn);
}
return (
<div>
<Toggle isOn={isOn} onChange={handleToggleChange} />
<div>Toggle is {isOn ? 'ON' : 'OFF'}</div>
</div>
);
}
export default App;
자식이 부모에게 명령을 내리거나 데이터를 전달하려면 부모로부터 제어권(props)들을 넘겨받아야한다
context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있습니다.
로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있습니다. Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있습니다.
애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됩니다. 아래 멀리 떨어진 많은 컴포넌트가 그 값을 변경하고싶어할 수 있습니다.
context 문제점
성능 이슈
Context API는 컴포넌트 트리의 모든 수준에서 데이터를 전달할 수 있지만, 종종 성능 문제를 일으킬 수 있습니다. Context를 사용하면 Context의 데이터가 변경될 때마다 해당 Context를 사용하는 모든 컴포넌트가 리렌더링됩니다. 이는 특히 대규모 애플리케이션에서 성능 저하를 유발할 수 있습니다.
스케일링 문제
작은 규모의 프로젝트에서는 효과적이지만, 애플리케이션이 커지면서 상태 관리가 복잡해질 때 Context API만으로는 관리가 어려워질 수 있습니다. 복잡한 상태 로직을 관리하고, 다양한 상태 변화를 효율적으로 처리하기 어려울 수 있습니다.
구조적 한계
Context API는 전역 상태 관리보다는 주로 테마, 사용자 설정과 같이 고정적이고 간단한 데이터를 공유하는 데에 적합합니다. 복잡한 상태 변화와 비즈니스 로직을 포함하는 더 동적인 상태 관리에는 한계가 있습니다.
이러한 문제점을 해결하기 위해 다른 전역상태를 관리하는 툴을 많이 쓰는데 recoil,redux toolkit 등... 개인적으로 가장 많이 사용하는 redux toolkit을 사용하는게 좋습니다
여기서 삼항연산자로 true일경우 <Counter isFancy={true}/> false일 경우 <Counter isFancy={false}/>을 보여주는데 체크박스로 isFancy값을 변경시켜도 Counter에 있는 count값은 변화하지 않는데 같은 자리의 같은 컴포넌트는 state를 보존합니다
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
return (
<article>
<h1>Scientists</h1>
<ul>{listItems}</ul>
</article>
);
}
Key는 각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 나중에 일치시킬 수 있도록 합니다
이는 배열 항목이 정렬 등으로 인해 이동하거나 삽입되거나 삭제될 수 있는 경우 중요해집니다
key를 잘 선택하면 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리에 올바르게 업데이트 하는데 도움이 됩니다
데스크탑의 파일에 이름이 없다고 상상해 보세요. 대신 첫 번째 파일, 두 번째 파일 등 순서대로 파일을 참조할 것입니다. 익숙해질 수도 있지만, 파일을 삭제한다면 혼란스러워질수도 있습니다. 두 번째 파일이 첫 번째 파일이 되고 세 번째 파일이 두 번째 파일이 되는 식으로 말이죠.
폴더의 파일 이름과 배열의 JSX key는 비슷한 용도로 사용됩니다.
이를 통해 형제 항목 간에 항목을 고유하게 식별할 수 있습니다.
잘 선택된 key는 배열 내 위치보다 더 많은 정보를 제공합니다. 재정렬로 인해 위치가 변경되더라도 key는 React가 생명주기 내내 해당 항목을 식별할 수 있게 해줍니다
Key 특징
key는 형제 간에 고유해야 합니다하지만 같은 key를다른배열의 JSX 노드에 동일한 key를 사용해도 괜찮습니다.
key는 변경되어서는 안 되며그렇게 되면 key는 목적에 어긋납니다! 렌더링 중에는 key를 생성하지 마세요.
Key 주의점
인덱스를 key로 사용하면 종종 미묘하고 혼란스러운 버그가 발생하니 사용하지마세요
마찬가지로 key={Math.random()}처럼 즉석에서 key를 생성하지 마세요. 이렇게 하면 렌더링 간에 key가 일치하지 않아 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있습니다. 속도가 느려질 뿐만 아니라 리스트 항목 내부의 모든 사용자의 입력도 손실됩니다. 대신 데이터 기반의 안정적인 ID를 사용하세요
📝이벤트 핸들링
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
React의 경우 함수의 값을 호출하는게 아닌 함수의 주소값을 전달하는 것입니다 → React에서 정한 문법이기 때문에 따라야합니다
리액트는 Facebook에서 만든 Javascript 라이브러리로 핵심 요소는 가상 DOM으로 인한 빠른 렌더링 기술을 가진다
📝리액트 장점
가상 돔을 이용한 빠른 렌더링
가상돔을 이용해 필요한 부분만 렌더링해 속도의 최적화가 가능합니다
클라이언트 사이드 렌더링
클라이언트 측에서 렌더링하기 때문에 서버에 부담이 없어집니다
컴포넌트 재활용
클래스를 생성하고 객체를 만들어서 다양한 곳에 사용하는 것과 동일합니다
개인적인 의견 → 물론 사용해보니 여러명에서 개발할 때 공통 컴포넌트 자체를 만들기는 쉽지 않아보입니다 다양한 상황에는 다양한 조건들이 들어가는데 어떤 건 살짝만 다른데 코드가 더 들어가면서 코드가 덕지덕지 많아지며 점점 복잡해지니 많은 경험과 리팩토링도하면서 어떤 걸 공통으로 만들어야할지에 대한 개념을 익혀야합니다
React Native (크로스 플랫폼 개발)
React 문법을 익혀두면 React Native로 크로스 플랫폼 개발이 가능해 iOS, AOS 개발이 가능합니다
개인적인 의견 → 현재 React Native의 버전이 아직도 1버전이 안 된 거보면 매우 불안정한 상태이고 공식적인 라이브러리도 따로 제공하지 않는 걸로 압니다 이 부분은 남이 만든 걸 가져다 쓰는데 업데이트가 주기적으로 이루어지지 않으면 운영하다 버그가 날 가능성이 높아보입니다 현재 React도 비슷한 형태이지만 앱개발엔 많은 기술들이 들어가는데 React Native에서 기본적으로 제공하는 게 많이 없다면 npm에 의존할 수 밖에 없을 것 가텐요
📝리액트 단점
러닝커브가 크다
상태관리와 렌더링에 대한 개념이 어렵고 새롭기 때문에 기존 HTML과 Javascript로 개발하는 것과 다른 점이 있어서 난해할 수 있다
SEO 적용이 힘들다
일반적으로 Server에서 그려져서 오면 검색엔진 봇이 내용을 읽어서 검색시 노출되게 할 수 있는데 React의 경우 Client Side Rendering이 주요 기술이라 Client가 제공해주는 js내용을 다운 받아서 그리기 때문에 검색엔진 봇이 들어왔을 때 아무 내용이 없어서 검색단어들을 가져갈 수 없는 것이죠 현재는 React에서도 Server Side를 지원하고 SEO 최적화를 위한 기술들을 제공하기 때문에 큰 문제가 되진 않아보입니다
초기 로드시간이 느리다
CSR 방식을 이용하면 모든 스크립트와 리소스가 다운로드 될 때까지 기다려야 하기 때문에 첫 페이지 로드가 느릴 수 있습니다
📝리액트 동작과정 (Virtual Dom)
일반적으로 웹 브라우저의 동작은 웹 브라우저가 웹사이트의 텍스트 문서를 읽어서 DOM(Document Object Model)이라는 트리 구조로 바꾸어 사용자에게 보여주게 되는데, 웹 개발자들은 사용자의 반응에 따라 이 DOM 트리를 변경하여 웹사이트의 내용을 갱신합니다 (실제로 jQuery나 Vanila를 이용해 수정하게끔 이벤트를 만들어서 테스트하면 최상위 body의 값이 깜빡거리면서 전체를 렌더링하는 걸 확인할 수 있습니다) 하지만 매번 작은 변화가 있을 때마다 전체 화면을 새로 그리는 것은 성능에 좋지 않습니다
React는 이 문제를 해결하기 위해 Virtual DOM을 도입합니다
Virtual Dom은 위 그림처럼 어느 부분이 변경 되어있는지 정확히 파악이 가능하기 때문에 변화를 주고 싶은 부분만 Virtual DOM에서 수정하게 됩니다
또한 DOM을 직접 조작하는 건 low-level이라 까다롭고 안정성이 떨어집니다 특히 규모가 커지면 엄청 많은 코드를 적어야할지도 모릅니다 그래서 돔으로 직접 조작하는 jQuery와 같은 라이브러리는 legacy 취급을 받습니다
📝Angular(앵귤러) vs Vue.js vs React
Angular, React, Vue 전부 비슷한 계열의 CSR이며 필요한 부분만 렌더링하는 기술들이 들어가있습니다
Angular
구글에서 만든 프레임워크로서 구글에서 만들었지만 2년째 업데이트가 안 되고 있네요 구글에서는 자주 가져다 버리는 프로젝트들이 많습니다 어떤 기술을 택할 때 중요한 부분이니 고려 바랍니다
Vue
1인 개발자가 만든 프레임워크인데 현재 다운로드수도 Angular에 비해 많고 React에 비해서 쉽다고 합니다
개인적으로 가장 많이 쓰면서 안정화된 버전이 나오고 최근에 업데이트되고 있는 기술을 쓰는게 좋습니다 고로 React를 사용하는게 좋다고 생각해요 물론 우리나라 취업시장을 고려하긴 해야합니다