리액트에서는 기존의 DOM 트리의 문제인 DOM이 깊어질 수록 구성 요소 변경에 대한 작업 비용이 많이 들기에 가상돔 이라는 새로운 개념을 만들어 기존의 DOM을 복사하여 리액트 내부 메모리에 저장하고, 리액트에서 어떤 코드의 변경 사항이 발생하면 이렇게 복사한 DOM을 사용해서 변경 사항을 처리하며, 최종적으로 실제 DOM 과 비교하여 변경된 부부만 업데이트 한다.
![[Pasted image 20240609105912.png|500]]
리액트 컴포넌트를 스타일링 하는 방법에는 4가지 방식이 있다.
인라인 스타일은 말 그대로 HTML 태그의 style 속성을 사용해서 CSS 스타일을 지정하는 방식을 말한다.
리액트에서는 style 속성을 객체 형식의 값으로 할당해야하고, font-size와 같은 케밥 케이스 형식의 속성은 카멜 케이스 형식으로 fontSize로 작성해야 한다.
const App = () => {
return (
<div>
<h1
style={{
fontSize: "30px",
color: "#ed4848",
textDecoration: "line-through",
}}
>
Hello World!
</h1>
</div>
);
};
export default App;
외부 스타일은 별도의 CSS 파일에 CSS 코드를 작성하고, 리액트 컴포넌트 파일과 연결해서 사용하는 방법을 말한다.
import "./App.css";
const App = () => {
return (
<div>
<h1 className="title">Hello World!</h1>
</div>
);
};
export default App;
.title {
font-size: 30px;
color: #ed4848;
text-decoration: line-through;
}
모듈 방식을 사용해서 특정 컴포넌트에만 종속되는 CSS 코드를 작성하기 위한 방법, 외부 스타일 방법과 비슷하지만, 파일명이 .module.css 라는 특징이 있음
.title {
font-size: 30px;
color: #ed4848;
text-decoration: line-through;
}
import styles from "./App.module.css";
const App = () => {
return (
<div>
<h1 className={styles.title}>Hello World!</h1>
</div>
);
};
export default App;
Utility-First 컨셉을 가진 CSS 프레임워크
Utility-First: 미리 셋팅된 유틸리티 클래스를 활용하여 HTML 코드 내에서 CSS를 적용시키는 것
Bootstrap 프레임 워크와 비슷하게 생겼으며 HTML 코드 내에서, CSS 스타일을 만들 수 있게 해주는 CSS 프레임워크
npm install -D tailwindcss
npx tailwindcss init
export default {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "tailwindcss";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
css: {
postcss: {
plugins: [tailwindcss()],
},
},
});
CSS-in-JS는 JavaScript 내에서 CSS 스타일을 작성하고 관리하는 기법을 의미합니다. 이 방법은 전통적인 CSS 파일과는 다르게, 스타일을 JavaScript 코드 내에서 정의하고 컴포넌트와 함께 번들링하는 접근 방식입니다.
import styled from "styled-components";
const HelloWorld = styled.h1`
font-size: 30px;
color: #ed4848;
text-decoration: line-through;
&:hover{
color:blue;
}
`;
const App = () => {
return (
<div>
<HelloWorld>Hello, World!</HelloWorld>
</div>
);
};
export default App;
리액트에서 props
는 컴포넌트 간에 데이터를 전달하는 방법, props
는 부모 컴포넌트가 자식 컴포넌트에 전달하는 읽기전용 데이터이다. props
는 컴포넌트가 상위 컴포넌트로부터 받는 입력값이라고 할 수 있으며, 컴포넌트는 이를 이용해 렌더링 결과를 동적으로 변경할 수 있다.
<Component 속성={값} />
import Greeting from "./components/Greeting";
const App = () => {
return (
<div>
<Greeting name="철수" />
<Greeting name="영희" />
<Greeting name="바둑이" />
</div>
);
};
export default App;
cont Greeting = (props:{name : string}) => {
return <h1>Hello, {props.name} </h1>
}
export default Greeting
props는 읽기 전용 데이터로 자식 컴포넌트에서 직접 수정이 불가능하다
타입스크립트에서는 props의 타입을 검사할 수 있다.
부모에서 자식 컴포넌트로 데이터를 전달하는 또 하나의 방법
type ChildProps ={
children: React.ReactNode; //리액트 노드 타입의 자식요소를 받음
}
const App = () => {
return <User>안녕하세요</User> //props children으로 데이터 전달
}
// 자식 컴포넌트에서 children값 받기
const User(props:ChildProps) {
const {children} = props //props를 구조 분해 할당으로 받음
return <div>{children}</div> //안녕하세요
}
const default App;
type ButtonProps = React.ComponentProps<'button'> & {
children: React.ReactNode;
style?: string
onClick?: ()=> void; // 클릭 이벤트 핸들러
}
const Button = (props: ButtonProps) => {
const {children, style, ...rest} = props;
return (
<button className = {style} {...rest}>
{children}
</button>
)
}
export defulat Button;
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0); // 초기값 count가 0으로
const clickHandler = () => {
console.log(count);
// count++; 직접 접근 불가능!!
setCount(count + 1);
};
return (
<>
<h1>Count : {count}</h1>
<button onClick={clickHandler}>증가</button>
</>
);
};
export default App;
useState
의 타입은 내부적으로 제네릭을 사용하는데 이는 상태의 타입을 지정하는데 사용된다. 즉 처음부터 useState
를 호출할 때 상태의 초기값으로 사용해 타입을 추정하여 지정한다.
const [count, setCount] = useState<number>(0)
const App = () => {
const [inputStr, setInput] = useState('')
const onChnageHandler = (value: string) => {
setInput(value);
};
return (
<>
<h1>{inputStr}</h1>
<input
type='text'
value={inputStr}
onChnage={(e)=>setINput(e.target.value)}>
</input>
</>
)
}
onChnage 안에 콜백함수 형태로 전달 받는 이유는 매개변수로 전달 받을 값이 있을때 콜백함수 형태로 넘겨준다. 바로 값이 나오면 안되고 이벤트 발생 시점에 함수가 실행되어야 하기 때문
리액트 컴포넌트 내에서 DOM 요소에 접근할 때 사용됨, 이 훅을 사용하면 함수 컴포넌트 내에서 DOM 요소를 선택하고 조작할 수 있다.
const inputEl = useRef<HTMLinputElement>(null);
const onButtonClick = () => {
inputEl.current?.focus()
}
return (
<div>
<input ref={inputEl} type='text />
<button onClick={onButtonClick}> Focus the input</button>
</div>
)
import React, { useRef, useState } from "react";
import "./Login.css";
const Login = () => {
const passwordEl = useRef<HTMLInputElement>(null);
const [isType, setIsType] = useState<string>("register");
const [data, setData] = useState({
name: "",
email: "",
password: "",
agree: false,
});
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setData((prev) => ({
...prev,
[e.target.name]:
e.target.type !== "checkbox" ? e.target.value : e.target.checked,
}));
};
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (data.password.length < 4) {
alert("비밀번호는 4자리 이상이어야 합니다.");
passwordEl.current?.focus();
return;
}
if (isType === "register") {
alert(
`이름:${data.name}, 이메일:${data.email}, 비밀번호:${data.password} 가입 완료!`
);
} else {
alert(`이메일:${data.email}, 비밀번호:${data.password} 로그인 완료!`);
}
};
// 입력 필드 유효성 검사
const isFormValid = () => {
if (isType === "register") {
return (
data.name.trim() !== "" &&
data.email.trim() !== "" &&
data.password.trim() !== "" &&
data.agree
);
}
return (
data.email.trim() !== "" && data.password.trim() !== "" && data.agree
);
};
return (
<div className="login">
<h2 className="login-title">
{isType === "register" ? "Login" : "Sign"} Into App
</h2>
<p className="login-subtitle">Please enter your details to continue.</p>
<form action="" onSubmit={onSubmitHandler}>
<div className="login-input">
{isType === "register" && (
<input
type="text"
name="name"
placeholder="Enter Your Name"
autoComplete="off"
value={data.name}
onChange={onChangeHandler}
/>
)}
<input
type="email"
name="email"
placeholder="someone@example.com"
autoComplete="off"
value={data.email}
onChange={onChangeHandler}
/>
<input
ref={passwordEl}
type="password"
name="password"
placeholder="Enter Password"
value={data.password}
onChange={onChangeHandler}
/>
<div className="login-checkbox">
<input
type="checkbox"
id="agree"
name="agree"
checked={data.agree}
onChange={onChangeHandler}
/>
<label htmlFor="agree"></label>
<span>
I agree with <em>terms</em> and <em>policies</em>
</span>
</div>
</div>
<div className="login-button">
<button
type="submit"
className="signin-btn"
disabled={!isFormValid()}
>
Sign In
</button>
<button
type="button"
className="login-btn"
onClick={() =>
setIsType((prev) => (prev === "register" ? "login" : "register"))
}
>
{isType === "register" ? "Go To Login" : "Go To Sing up"}
</button>
</div>
</form>
</div>
);
};
export default Login;
조금 더 복잡한 상태 관리를 할 때 사용하는 리액트 훅이다.
const [todos, dispatch] = useReducer(TodoReducer, []);
useReducer(외부 컴포넌트에서 처리할 로직, 초기값 )
dispatch({type: 'ADD_TODO'}) // {type: 'ADD_TODO'} -> action
import {v4 as uuidv4} from 'uuid';
export type Todo = {
id: string;
text: string;
completed: boolean;
};
export type Action = {
type: 'ADD_TODO' | 'TOGGLE_TODO' | 'DELETE_TODO';
payload: string; //Partial은 모든 속성을 선택적으로 만듭니다.
};
// [{id, text,completed}]
export const TodoReducer = (state: Todo[], action: Action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, {id: uuidv4(), text: action.payload, completed: false}];
case 'TOGGLE_TODO':
return state.map((todo) =>
todo.id === action.payload
? {...todo, completed: !todo.completed}
: todo
);
case 'DELETE_TODO':
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
};
const
import React from 'react';
import Checkbox from './html/Checkbox';
import {Action, Todo} from './reducer/TodoReducer';
type TTodoListItemProps = {
todo: Todo;
dispatch: React.Dispatch<Action>;
};
const TodoListItem = (props: TTodoListItemProps) => {
console.log('TodoListItem rendering');
const {todo, dispatch} = props;
return (
<>
<li className="flex justify-between items-center py-[10px] px-[15px] bg-[rgba(53,56,62,0.05)] border border-[#4f4f4f] rounded-[4px]">
<Checkbox
checked={todo.completed}
onChange={() => dispatch({type: 'TOGGLE_TODO', payload: todo.id})}
>
<span
className={`${todo.completed && 'line-through'} text-[#35383E]`}
>
{todo.text}
</span>
</Checkbox>
<button
className="w-6 h-6 bg-[rgba(53,56,62,0.1)] border border-[#4F4F4F] rounded flex items-center justify-center"
onClick={() => dispatch({type: 'DELETE_TODO', payload: todo.id})}
>
<svg
width="15"
height="16"
viewBox="0 0 15 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.50002 9.81827L12.9548 15.2731L14.7731 13.4548L9.31829 8L14.7731 2.54518L12.9548 0.726901L7.50002 6.18173L2.04519 0.726902L0.226918 2.54518L5.68174 8L0.226919 13.4548L2.04519 15.2731L7.50002 9.81827ZM7.50002 9.81827L9.31829 8L7.50002 6.18173L5.68174 8L7.50002 9.81827Z"
fill="#4F4F4F"
/>
<path
d="M7.50002 9.81827L9.31829 8L7.50002 6.18173L5.68174 8L7.50002 9.81827Z"
fill="#4F4F4F"
/>
</svg>
</button>
</li>
</>
);
};
export default React.memo(TodoListItem);
//reducer를 사용하면 메모제이션이 쉽게 된다.
//dispatch를 하면 콜백을 안 해도 된다.
HTTP 프로토콜: TCP/IP 기반으로 클라이언트와 서버 사이테 이루어지는 요청/응답 프로토콜
QUIC 프로토콜: UDP 기반으로 빠른 전송 속도를 가지는 프로토콜
const App = () => {
const fetchData = () => {
fetch('http://localhost:4000/todos')
.then((res)=>{
return res.json()
})
.then((data)=>{
console.log(data)
})
}
}
Fetch API
는 브라우저 내장 api로 Promise
기반으로 작동한다. 응답이 Promise 객체로 돌아오므로 해당 response를 json 형태로 변환하고 then을 사용해 json 형태의 응답에서 data만 받아 할당해야 한다.
async/await
를 사용해 then 상태를 생략할 수 있다. async 함수는 항상 promise를 반환 하고 await은 promise가 해결 될때 까지 기다리고 그 결과값을 리턴한다.
const fetchDataAsync = async() => {
try{
const res = await fetch('http//localhost:4000/todos')
const data = await res.json();
setTodos(data)
}catch(err) {
console.log(err)
}
}
//fetch 데이터를 더 간략하게 표현
//const res = await(await fetch('http:localhost:4000/todos')).json()
axios 외부 라이브러리 Promise 기반이며 위의 방법들보다 더 간결하고 많은 기능을 사용할 수 있다. 자동으로 json 데이터를 직렬화/역직렬화 한다.
const axiosGet = () => {
axios.get('http://localhost:4000/todos')
.then((res)=> {
console.log(res)
setTodos(res.data)
})
}
async/await
를 사용하면 axios 요청을 처리하면 코드가 간결해진다.
const axiosAsyncGet = async () => {
try {
const res = await axios.get('http;//localhost:4000/todos');
setTodos(res.data)
}
}
사이드 이펙트 - 컴포넌트 외부에 있는 데이터를 읽거나수정하는 작성을 수행할 수 있도록 하는 hook
useEffect(()=>{
// 부수 효과를 수행하는 코드
종속성 배열을 비워둔거면 이안에 있는 코드가 렌더링 영향 없이 한번만 실행된다.
fetchData();
return ()=> {
언마운트 될 때 실행된다. 즉 컴포넌트가 화면에서 사라질 때 실행된다.
}
})
리액트에서 상태를 변경해도 1씩 증가하는 이유는 상태 업데이트가 비동기적으로 수행되고, 여러 상태 업데이트가 하나의 렌더링 주기에 배치되기 때문에 리액트의 상태 업데이트 매커니즘은 성능 최적화를 위해 여러 상태 변경을 배치하여 한 번의 렌더링 주기 동안 처리 한다.
비용이 큰 연산에 대한 결과를 저장해 두고, 이 저장된 값을 반환하는 훅
React.memo()
: 컴포넌트를 메모이제이션useCallback()
: 함수를 메모이제이션useMemo()
: 값 자체를 메모이제이션import React from "react";
const Display = ({ count }: { count: number }) => {
console.log("display rendered");
return (
<div>
<h1>COUNT : {count}</h1>
</div>
);
};
export default React.memo(Display);
컴포넌트의 메모이제이션 count 값이 바뀌지 않는 이상 재렌더링 되지 않는다.
React.memo는 컴포넌트의 props가 변경되지 않았을 때만 렌더링을 방지한다. 그러나 부모 컴포넌트가 렌더링될 때 마다 자식 컴포넌트에 새로운 콜백함수를 전달하는 경우, 자식 컴포넌트는 새로운 콜백 함수를 받게 되므로 재렌더링될 수 있다.
그래서 이걸 해결하기 위해 부모 컴포넌트에서 콜백 함수가 아닌 메모이제이션 된 콜백 함수를 전달해야 한다.
const onClickhandler = useCallback(()=>{
setCount(count + 1)
},[count])
두번째 인자로 의존성 배열을 준다. 재렌더링 할때도 함수를 다시 만들지 말고 재활용한다. 즉 App 컴포넌트는 루트 컴포넌트이므로 매번 재렌더링 되는데 이 때 해당 함수는 제외된다.
그리고 종속성 배열을 전달하여 값이 변경될 때에만 콜백함수가 생성되도록 한다.
아니면 이전 상태값을 참조하는 것도 방법이다 e.g) setCount((prev)=> prev +1)
cont initialItems = new Array(29_999_999).fill(0).map((-,1) => {
return{
id: 1,
selected: i === 29_999_998,
}
})
첫번째 인수로 어떠한 값을 반환하는 생성 함수를, 두 번째 인수로 해당함수가 의존하는 값의 배열을 전달한다. useMemo는 렌더링 발생 시 의존성 배열의 값이 변경되지 않으면 함수를 재실행하지 않고 이전에 기억해둔 값을 반환한다.
zustand는 리액트 상태 관리를 위한 라이브러리이다. 다른 상태관리 라이브러리와는 다르게 직관이적이며 심플하다.
단일 스토어 패턴을 사용해 상태를 관리하는데 전역 상태를 하나의 객체에 저장하고 필요한 컴포넌트에서 import 해 사용한다. 액션은 상태 객체에 변경을 줄 수 있는 함수이다. 상태 객체의 메서드로 액션이 정의된다.
npm install zustand
import create from 'zustand'
//zustand 스토어 생성
const useStore = create((set)=> {
//인자로 set을 받음
count: 0,
increment: () => set((state)=> ({count: state.count + 1}))
decrement: () => set((state)=> ({count: state.count - 1})),
})
export default useStore
import useStore from './store'
const App () = ()=> {
const count = useStore((state)=> state.count)
const increment = useStore((state) => state.increment)
const decrement = useStore((state)=> state.decrement)
return(
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
export default App;
zustand는 비동기적인 작업을 처리할 코드를 작성할 수 있지만 메모이제이션을 통해 성능 최적화를 하기 위해서는 필요한 상태나 액션만 선택해서 사용해야한다. 그렇지 않으면 메모이제이션 적용이 안 된다.
const Counter () = () => {
const count = useStore((state)=> state.count) // count만 선택
const increment = useStore((state)=> state.increment) // increment만 선택
}
count 와 increment 중 필요한 부부만 재렌더링이 된다.
npm install react-router-dom
#
이 붙는 형식 /#/loginimport { BrowserRoute, HashRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
<Routes>
<Route element={<Home />} path="/" />
<Route element={<Cart />} path="/cart" />
</Routes>
<Routes>
...
<Route element={<CartDetail />} path="/cart/:id" />
...
</Routes>
일반 경로나 동적 경로는 모두 쿼리 스트링을 가질 수 있다.
쿼리 스트링이나 URL 뒤에 ? 나 &로 추가적인 정보가 붙는 형식을 말한다.
const CartDeatil = () => {
const [searchParams] = useSearchParams();
console.log(searchParams.get(key))
return <div>CartDetail</div>
}
export default CartDetail;
createBrowerRouter 메서드안에 컴포넌트와 경로를 넣고 변수에 할당하여 그 변수를 RouterProver prop 라우터 객체로 전달해준다.
const router = createBrowerRouter([
{
element: <DefaultLayout />,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/login',
element: <Login />,
},
{
path: '/register',
element: <Register />,
},
],
},
])
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
react는 라이브러리라 부름 -파일의 구조나 폴더 구조 개발자가 직접 세팅을 함 ex) 라우팅 - 직접 라우팅 설치해서 설정해야함
vue.js,Nuxt.js, Next.js는 프레임워크라 부름 - 정해진 규칙을 따라야 함
![[Pasted image 20240607171102.png | 500]]
cd [폴더이름] -> 폴더로 이동
./ 현재 폴더
../ 상위폴더
../../ 상위 폴더 두번
![[Pasted image 20240607160352.png | 500]]
npm init - y
npm install react@latest next@latest react-dom@latest
"start" : "next dev"
보일러 플레이트(boilerplate)란 주로 프로그래밍이나 법률 문서에서 반복적으로 사용되는 표준화된 코드나 텍스트를 의미, 이 용어는 원래 철강 산업에서 철판을 의미하는 단어였으나 다양한 분야에서 공통적으로 사용되는 표현이나 코드 블록을 지칭
대표적으로 CRA, create-vite
npx create-next-app@latest
/about
경로에 대한 라우트는 "About us" 페이지 컴포넌트와 연결됩니다.정의 Next.js 14에서 새롭게 도입된 라우팅 시스템
![[Pasted image 20240607172336.png ]]
Next.js는 App routing 방식과 pages routing 방식 둘 다 지원한다.
14 버전에서는 App routing 방식으로 변경 되었다.
app/
|-- blog/
|-- page.tsx --- /blog
|-- about/
|-- page.tsx ---/about
앱 라우터는 루트에 app 폴더 생성하면 자동으로 라우트가 생성이 된다. app 폴더 하위에 blog 페이지를 만들고 page.tsx 컴포넌트를 생성하면 /blog
app/
|- blog/
|-- page.tsx - /blog
|-- first/
|-- page.tsx -- /blog/first
동적(dynamic) 경로를 사용하면 특정 세그먼트가 동적으로 변경되는 경로를 지정 할 수 있음
app/
|--blog/
|--page.tsx- /blog
|--[id]/
|--comment/
|--[reviewId]/
|-- page.tsx - /blog/2/comment/1
특정 경로 이하의 모든 경로를 포괄적으로 허용하는 라우트 지정 방식
app/
|-docs/
|--[...slug]
|--page.tsx -/docs/1 or /docs/1/api or /docs/1/api/ -- 모든경로 커버
const Docs =({
params,
}): {
params: {
slug: string[];
}
}) => {
console.log(params) // docs 이후의 경로가 배열로 들어감 ex/
}
app
|-- _utils
|-page.tsx- 접근 불가
form-date.ts
루트 경로에 not-found.tsx 파일은 경로를 찾지 못했을 때 보여줄 페이지다. next.js에서는 not-found 파일을 특정 그룹에 커스텀해서 작성할 수 없다.
루트 레이아웃은 모든 페이지에 공통적으로 사용되는 header와 footer가 포함된다.
export const metadata = {
title: {
// %s는 하위 layout에 metadata에서 title을 받아와서 적용이 된다.
template: '&s | funcoding'
default : 'Next.js | funcoding'
},
description: 'First Next.js'
}
const RootLayout = ({children} : {children: React.ReactNode})=> {
return (
// 이래야 html body 태그가 생성된다.
<html lang="en">
<body>
<h1>Root Header</h1>
{children}
<Link href="/blog">로그인</Link>
<h1>Root Footer</h1>
</body>
</html>
);
}
현재 페이지의 경로를 가져오는 리액트 라우터의 훅이다
const pathname = usePathname()
next.js 는 두 가지 컴포넌트가 존재하며 두 번의 렌더링을 하는데 서버 컴포넌트는 서버 측에서 실행되는 리액트 컴포넌트로 SSR을 지원하며 초기 HTML 을 생성한다. 생성된 HTML을 클라이언트로 전달해 웹페이지의 초기 로딩 시간을 줄이고 SEO를 향상 시킨다.
클라이언 컴포넌트는 브라우저에서 실행되는 리액트 컴포넌트로 CSR을 지원해 서버에서 초기 HTML을 전달 받고 이후에 클라이언트에서 JS를 적용하며 초기 html에 살을 붙이는 역할을 한다 하여 뼈대에 수분을 공급하는 기능인 하이드레이션이라고 불리는 개념이다.
use client
를 쓰면 해당 컴포넌트는 클라이언트 컴포넌트이다.
import {Roboto} from 'next/font/google'
const roboto = Rotobo({weight:['400','700'], subsets:['latin']})
<h1 className = {roboto.className}> hello, world! <h1>
본 후기는 본 후기는 [유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 1기 과정(B-log) 리뷰로 작성 되었습니다.