SPA의 단점을 보완하는 다양한 기술 도입
<div class="todolist">
<ul>
<li>두부</li>
<li>계란</li>
<li>라면</li>
</ul>
</div>
/*#__PURE__*/_jsx("div", {
class: "todolist",
children: /*#__PURE__*/_jsxs("ul", {
children: [/*#__PURE__*/_jsx("li", {
children: "\uB450\uBD80"
}), /*#__PURE__*/_jsx("li", {
children: "\uACC4\uB780"
}), /*#__PURE__*/_jsx("li", {
children: "\uB77C\uBA74"
})]
})
});
상태 변경 시 뷰를 렌더링할 때, 브라우저 DOM에 바로 적용하지 않고, 브라우저 DOM과 유사한 트리 구조의 가상 DOM(자바스크립트 객체)을 먼저 수정. 이후 수정 전후의 가상 DOM을 비교하여 바뀐 부분만 실제 브라우저 DOM에 반영
DOM API를 이용한 화면 갱신 방법
npx create-react-app cra
# 생성한 프로젝트 폴더로 이동
cd cra
# 개발 서버 실행
npm start
npm init vite@latest
# 생성한 프로젝트 폴더로 이동
cd vite
# 필요 패키지 설치
npm i
# 개발 서버 실행
npm run dev
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
resolve: {
alias: [
{ find: "@", replacement: "/src" },
{ find: "@components", replacement: "/src/components" },
{ find: "@pages", replacement: "/src/pages" },
{ find: "@hooks", replacement: "/src/hooks" },
],
},
})
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["/*"],
"@components/*": ["components/*"],
"@pages/*": ["pages/*"],
"@hooks/*": ["hooks/*"],
}
}
}
npm run build
npx serve -s build
npm run preview
1 단일 루트 요소를 반환해야 한다.
Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>…</>? 에러 발생
return (
<h1>Todo List</h1>
<div>...</div>
);
return (
<div>
<h1>Todo List</h1>
<div>...</div>
</div>
);
return (
<Fragment>
<h1>Todo List</h1>
<div>...</div>
</Fragment>
);
return (
<>
<h1>Todo List</h1>
<div>...</div>
</>
);
2 모든 태그는 닫는다.
<img src="./logo.png">
<br>
<ul>
<li>두부
<li>계란
<li>라면
</ul>
<img src="./logo.png" />
<br />
<ul>
<li>두부</li>
<li>계란</li>
<li>라면</li>
</ul>
3 요소의 속성명은 카멜 표기법(camel case)을 준수해야 한다.
<div id="todolist" class="todo"></div>
document.querySelector('#todolist').className = 'todo';
<div id="todolist" className="todo"></div>
const todoClass = 'todo';
<div id="todolist" className={ todoClass }></div>
4 보간법{ }을 사용할 때에는 표현식을 사용해야 함
{ item.done ? <s>두부</s> : '두부' }
{
for(let i=0; i<itemList.length; i++){
return item.title;
}
}
{ itemList.map(item => item.title) }
5 보간된 HTML 문자열은 인코딩됨
const App(){
const msg = '<i>World</i>';
return <span>Hello { msg }</span>
}
// 만들어지는 문자열: <span>Hello <i>World</i></span>
const App(){
// { msg }를 <span dangerouslySetInnerHTML=></span>로 변경
const msg = '<i>World</i>';
return <span>Hello <span dangerouslySetInnerHTML=></span></span>
}
const App(){
// const msg = '<i>World</i>';
const msg = <i>World</i>;
return <span>Hello { msg }</span>
}
// ch02-start/todo/03.html
function App(){
const title = 'React Props';
let list = [
{ _id: 1, title: '리그오브 레전드', done: false},
{ _id: 2, title: '영화 보기(집에서)', done: false},
{ _id: 3, title: '던파', done: false},
];
return (
<div id="app">
<div>
<Title title={ title } />
<TodoList list={ list } />
</div>
</div>
);
}
function Title({ title='Default Title' }){
return (
<div>
<h1>Simple Todo List - { title } :()</h1>
<hr />
</div>
);
}
function TodoList({ list }){
const itemList = list.map(item => {
return (
<li key={ item._id }>{ item.title }</li>
);
});
return (
<ul className="todolist">
{ itemList }
</ul>
);
}
function Profile(props) {
return (
<div>
<Avatar { ...props } />
</div>
);
}
const [state, setState] = useState(initialState);
const [firstName, setFirstName] = useState('Dragon');
if(firstName === 'Dragon'){
const [lastName, setLastName] = useState('Gil');
}
const [age, setAge] = useState(36);
{
"_id": 4,
"email": "u1@market.com",
"name": "데이지",
"phone": "01044445555",
"address": "서울시 강남구 논현동 222",
"type": "user",
"createdAt": "2024.01.25 21:08:14",
"updatedAt": "2024.02.04 09:38:14",
"extra": {
"birthday": "11-30",
"membershipClass": "MC02",
"addressBook": [
{
"id": 1,
"name": "회사",
"value": "서울시 강동구 천호동 123"
},
{
"id": 2,
"name": "집",
"value": "서울시 강동구 성내동 234"
}
]
}
}
npm i immer
const newAddressBook = user.extra.addressBook.map(address => {
if(address.id === Number(e.target.name)){
return { ...address, value: e.target.value };
}else{
return address;
}
});
const newState = {
...user,
extra: {
...user.extra,
addressBook: newAddressBook
}
};
setUser(newState);
import { produce } from 'immer';
...
const newState = produce(user, draft => {
const address = draft.extra.addressBook.find(address => address.id === Number(e.target.name));
address.value = e.target.value;
});
setUser(newState);
npm i prop-types
import PropTypes from 'prop-types';
import TodoItem from "./TodoItem";
function TodoList(props){
const list = props.itemList.map(item => <TodoItem key={ item.no } item={ item } toggleDone={ props.toggleDone } deleteItem={ props.deleteItem } />);
return (
<ul className="todolist">
{ list }
</ul>
);
}
TodoList.propTypes = {
itemList: PropTypes.array.isRequired,
toggleDone: PropTypes.func.isRequired,
deleteItem: PropTypes.func.isRequired,
};
export default TodoList;
import PropTypes from 'prop-types';
function TodoItem(props){
return (
<li>
<span>{ props.item.no }</span>
<span onClick={ () => props.toggleDone(props.item.no) } >{ props.item.done ? <s>{ props.item.title }</s> : props.item.title }</span>
<button type="button" onClick={ () => props.deleteItem(props.item.no) } >삭제</button>
</li>
);
}
TodoItem.propTypes = {
// item: PropTypes.object.isRequired,
item: PropTypes.shape({
no: PropTypes.number,
title: PropTypes.any.isRequired,
done: PropTypes.bool
}).isRequired,
toggleDone: PropTypes.func.isRequired,
deleteItem: PropTypes.func.isRequired
};
export default TodoItem;
import PropTypes from 'prop-types';
MyComponent.propTypes = {
// 특정 JS 타입임을 선언(해당 속성이 전달 된다면 지정한 타입이어야 함)
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// 모든 종류의 자식 요소(리액트 엘리먼트, 문자, 숫자, 배열, 불린, null, undefined 등)
optionalNode: PropTypes.node,
// React 엘리먼트(JSX, React.createElement()로 생성된 엘리먼트)
optionalElement: PropTypes.element,
// React 컴포넌트(클래스 컴포넌트, 함수 컴포넌트, HTML 태그명)
optionalElementType: PropTypes.elementType,
// 특정 클래스의 인스턴스
// 이 경우 JavaScript의 instanceof 연산자를 사용
optionalMessage: PropTypes.instanceOf(Message),
// 열거형(enum)으로 처리하여 prop가 특정 값들로 제한되도록 할 수 있음
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// 여러 종류중 하나
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 특정 타입의 배열
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// 특정 타입의 프로퍼티 값들을 갖는 객체
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// 지정된 타입의 속성을 가지고 있는 객체(다른 속성이 있어도 됨)
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
// 지정된 타입의 속성만 가지고 있는 객체(다른 속성이 있으면 안됨)
optionalObjectWithStrictShape: PropTypes.exact({
name: PropTypes.string,
quantity: PropTypes.number
}),
// 위에 있는 모든 구문에 'isRequired'를 연결하면 해당 속성이 필수임을 나타냄
requiredFunc: PropTypes.func.isRequired,
// 타입은 상관없고 필수임을 나타낼때
requiredAny: PropTypes.any.isRequired,
// 사용자 정의 유효성 검사기를 지정
// 검사 실패 시에는 에러(Error) 객체를 반환해야 함
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
`'${componentName}' 컴포넌트의 prop '${propName}' 값 검증 실패.`
);
}
},
// 'arrayOf' 와 'objectOf'에 사용자 정의 유효성 검사기 지정
// 검사 실패 시에는 에러(Error) 객체를 반환해야 함
// 유효성 검사기는 배열(array) 혹은 객체의 각 키(key)에 대하여 호출됨
// propValue: 현재 검사 중인 prop의 값(배열이나 객체)
// key: 현재 검사 중인 prop의 키
// componentName: 현재 검사 중인 컴포넌트의 이름
// location: prop이 전달된 위치 ("props" 또는 "context" 중 하나)
// propFullName: prop의 이름
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
if (!/matchme/.test(propValue[key])) {
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
})
};
npm i react-hook-form