Bleeding edge

React 정리 - 수정중... 본문

Javascript/React & Next

React 정리 - 수정중...

codevil 2022. 6. 14. 15:06

리액트?

CBD(Component Based Development)

요즘 웹이 다른 프로그램처럼 기능이 많아지면서, 구현해야하는 기능이 많아졌다. 이로 인해서 Javascript가 복잡해지면서 하나의 script.js에 모든 코드를 넣기가 어려워졌다. 이로 인해서 어떤 script 코드가 어떤 element를 제어하는지 파악하기 힘들어지고,  가독성이 떨어졌으며, 변수 선언 시 겹치지 않도록 신경을 써줘야하는 불편함이 생겼다.


해결책 : 여러개의 css와 js 파일로 분리를 하였다.

1) 어떤 element

2)그 element를 꾸미기 위한 style

3) 그 element 동작에 대한 script

1), 2), 3)을 묶음을 Component라고 한다.

 

리액트의 특징?


프론트엔드에서 하는 제어의 대부분은 상태값이 변하면, UI가 변한다.

Javascript로 구현했을 때의 특징 = 상태에 따른 UI를 바꾸기 위해서 DOM 노드를 직접 조작해야 한다.

React =데이터가 바뀔 때 이 요소를 바꾸자가 아니라 이전 뷰를 날린 후 결과적으로 보여줘야할 뷰를 생각.

UI 영역에 변경이 발생하면 Component만 업데이트하는 것

= 리액트가 하는일

= 성능 개선

리액트는 내부적으로 가상돔이라는 것을 통해 변경된 부분만 업데이트한다

리액트의 특징 

1. 선언형 : 상호작용이 많은 UI를 간단한 뷰만 설계하고, 데이터 변경에 따라 적절한 컴포넌트만 효율적으로 갱신/랜더링합니다. 선언형 뷰는 코드를 예측 가능하고 디버그하기 쉽게 만들어 줍니다.

2. 컴포넌트 기반 : 스스로 상태를 관리하는 캡슐화된 컴포넌트를 만들어서, 복잡한 UI를 만들어보세요. 다양한 형식의 데이터를 앱 안에서 손쉽게 전달할 수 있고, DOM과는 별개로 상태를 관리할 수 있습니다.

프레임워크?


Web Application을 만들기 위해 필요한 여러가지 기능을 제공한다.

Web Application은 공통적으로 어떤 기능이 필요할까요? 리액트는 UI라이브러리라서, 직접 구현하거나 구축해야 합니다.

라우팅 : 어떤 url이 들어왔을 때 어떤 페이지를 보여줘야해 / React-Router

전역 상태 관리 : 페이지에서 여러 상태를 다루기 위한 것/Redux, Redux-thunk, Redux-saga, Recoil

API 호출 : 서버와의 통신을 위해서/Axios

 

React.createElement()함수?


const element = document.createElement(tagName[, options])
//지정한 tagName의 HTML 요소를 만들어서 반환

const element = React.createElement(component, props, ...children)
//ReactElement를 반환

React.createElement(component, props, ...children)

문자열 또는 리액트 컴포넌트 : 문자열 일때 HTML 태그 혹은 리액트 컴포넌트

Component가 가질수 있는 옵션들 : style, className

Component가 감싸고 있는 내부 컴포넌트 : 리액트 컴포넌트, 텍스트, React.createElement("p", null, "hello") innerHTML

 

React.render()함수?


ReactElement를 루트의 DOM 노드에 렌더링 하기 위해서 사용

 

Create React App 설치하기


React를 설치하기 위해서는 Node 14.0.0 혹은 npm 5.6 상위버전이 필요합니다. 이를 확인하기 위한 cli keyword

node -v
npm -v

- node와 npm 버전확인

Create React App를 설치하기 위한 cli keyword

pwd
// 현재위치 - C:\ 같은 것을 알려준다
ls
//현재 위치에 있는 폴더 혹은 리스트
cd folderName
//folderName(폴더이름)으로 위치 이동
mkdir folderName
//새로운 폴더를 만들어줌
npm init
//폴더 초기화

 

Create React App 설치한 후


package.json

"script" :{
	"start":"react-scripts start",
    "build":"react-scripts build",
    "test":"react-scripts test",
    "eject":"react-scripts eject",
}

npm start를 통해서 리액트를 실행시킬 수 있다.

 

리액트 폴더 선정 방법


react/src/ :리액트 컴포넌트를 만드는데 쓰이는 것들

react/public/ : react render을 하는 것들로 이루어져 있다.

 

React 앱을 만들기 위해 필수적인 개발 도구들


Babel, Webpack, HMR(hot-module-replacment) :

Babel : 어떤 코드의 형태를 A에서 B로 변환해주는 컴파일러(Babel - JSX ->Javascript)

 

Babel을 사용하는 이유 :

JSX는 자바스크립트 표준이 아니기에 브라우저가 JSX를 읽도록 바벨이 도와준다. 

바벨 설치 cli keyword

npm i -D @babel/core @babel@cli @babel/preset-react

//Babel/core 바벨을 사용하기 위한 것
//Babel/cli 바벨을 커맨드로 사용하기 위한것
//Babel/preset-react 바벨로 리액트를 컴파일 하기 위한 것
//-D package.json에 devDependencies를 추가한다

바벨 사용 cli keyword

npx babel src/components/fileName.js --presets=@babel/preset-react
npm i -D @babel/preset-env

 

Webpack :

webpack 설치 cli keyword

npm i -D webpack webpack-cli html-webpack-plugin babel-loader

webpack.config.js

module.exports ={
	mode:"development",
	entry:"./src/index.js" //index파일을 기준으로 엮여져있다를 의미
    output:{
    	//번들링을 한 파일을 어디에 위치 시킬지
    	path:path.resolve(__dirname, "dist"),
        //dist 폴더가 만들어지고 안에 output된 결과가 나오게 된다
        file:"bundle.js",
        //out된 파일 이름
    },
    module:{
    rules: [{test:/\.js$/, use:"babel-loader"}],
    //test ->모든 js파일에 적용하겠다, use ->바벨 로더는 웹팩이 불더들일 때도 jsx파일을 받는다
    //웹팩은 이를 해석하지 못하기 때문에, 바벨 로더로 받는다
    },
    optimization:{minimizer:[]},
    //웹팩은 자바스크립트 파일을 압축하지만 지금은 눈으로 확인해야 하기 때문에 압축기능을 끈다
}

add code in webpack.config.js

	devServer:{
    	static:"./dist",
        hot:true,
    }
npm install -D webpack-dev-server
npx webpack serve

 

 

JSX 소개


const element = <h1>Hello </h1>

위의 특이한 태그 문법은 무자열도, HTML도 아닙니다. 이는 JSX라하며 Javascript를 확장한 문법입니다. JSX는 React Element를 생성합니다.

1. JSX 표현식 포함하기

const name = "codev"
const element = <h1> Hello, {name}</h1>

중괄호를 통해서 표현할 수 있습니다

.

2. JSX도 표현식입니다

function func(x){
	if(x){
    	return <h1>Hello, {anotherFunc(x)}</h1>
    }
   	return <h1>Hello, stranger</h1>
}

 

3. JSX 속성 정의

const element1 = <a href="https://codevil.tistory.com"> link </a>
const element2 = <img src={user.avatarUrl}></img>

JSX는 HTMl보다는 Javascript에 가깝기 때문에, HTML 어트리뷰트보다는 camelCase 프로퍼티 명명 규칙을 사용합니다

예시 : class  => className, tabindex =>tabIndex

<div tabIndex="0"> //tabIndex사용법
	<img src={logo}/> //src를 {}로 사용하기
    <a 	href="https://codevil.tistory" className ="logo">//className 사용하기
        {func(x)}
    </a>
	<input type="checkbox" id="agree"/>
    <label htmlFor="agree">동의</label> //input, label을 위해 id와 htmlFor을 사용하는 것 주의!
</div>

 

JSX 문법에서 주의해야할 것

return(
	<a href="https://codevil.tistory.com">{
		if(name==="correctName") return `Hello, ${name}!`	
    }</a>
)

IF 문은 자바스크립트를 사용하는 것이지 자바스크립트 자체는 아니기 때문에, 3항 연산자를 사용해야한다.

 

Components 와 Props


컴포넌트를 통해 UI를 재사용 가능한 개별적인 여러 조각으로 나눈다.

Babel : 어떤 코드의 형태를 A에서 B로 변환해주는 컴파일러(Babel - JSX ->Javascript)

App.js

function App(){
	return(
    <div className ="App">
		<Header title ="{A}"/>
		<Header title ="{B}"/>
		<Header title ="{C}"/>
	</div>
    )
}

Header.js - title에 따른, header표시법

const Header = (props) =>{
	<header>
	<p>
	{props.title==="A"? <span>The title is A </span>
        :props.title==="B"? <span>The title is B</span>
        :<span>The title is C </span>}
	</p>
	</header>
}

Props의 특징

1. props는 읽기 전용입니다.2. 모든 컴포넌트에서 props.children을 사용할 수 있습니다. 

<Welcome title={"Hello"}/>
//props.title = "Hello"
<Welcome>Hello~</Welcome>
//props.children = "Hello~", document.querySelector("Welcome").innerHTMl같은 느낌이다

Props.children 활용

1. 사용자 정의 컴포넌트가 일반적인 html 태그와 구조가 비슷할 때

export default function Button(props){
	return <button onClick={props.handleClick}> {props.children}</button>
}

2. 컴포넌트에 다른 컴포넌트를 전달해야 할 때

<Layout>
	<Apage/>
</Layout>

다중 Props : function내에서 이름을 정해줄 수 있다

App.js

function App(){
	const info = {
    	firstName = "ha",
        lastName = "ha",
        withImg = "true",
    }
    return (
    	<Welcome {...info}>
    )
}

func.js

export default function func({withImg, firstName, lastName})
	{
    return (
    	<div>props.withImg</div>
        <div>props.firstName</div>
        <div>props.lastName</div>
    )
 }

 

 

state, setState


함수 컴포넌트는 함수다. 함수 컴포넌트에서 내부적으로 상태를 관리해야 하는 일이 필요하다.

setState()는 컴포넌트의 re-rendering을 발생시킨다.

불변성을 지키지 않고, 메모리 영역의 값을 직접 변경하면 리액트는 state가 변경되었다고 인지하지 못한다. 리액트는 전 stat와 후 state를 비교할 때 얕은비교를 하기 때문이다

원시 타입 state

import React, {useState} from "react"

export default function Counter(){
	const [count, setCount] = useState(0);
    const [show, setShow] = useState(true);
	return (
    	<button onClick ={() => setCount(count+1)}>+1</button>
        <button onClick ={() => setShow(!show)}>Show and hide</button>  
        {show && `Counter: ${count}`}
    )
}

참조 타입 state - object

export default function Counter(){
	const [info, setInfo] = useState({
    	count :0,
        show:true,
        operator: operators[0]
    })
	render(
    	<button onClick ={()=>{
        	if (info.operator==="+") result=info.count+1
            setInfo({...info, count:result})
        }}
    )
}

참조 타입 state - Array

const [array, setArray] = useState(["a","b","c","d"])
render(
	<button onClick ={()=>setArray([...array, newItem])}
	<button onClick ={()=>setArray([array.filter(arr=>{})])}
)

Props  vs State

props : 부모 컴포넌트가 자식 컴포넌트에게 전달하는 값. 값을 자식 컴포넌트가 변경할 수 없음

state : 자신이 스스로 관리하는 상태값. 값을 자신이 변경할 수 있다.

prop를 통해 값을 내려받거나, 자신이 관리하고 있는 state가 변경되면 컴포넌트 컴포넌트 렌더링이 발생한다.

 

state의 비동기


const add=()=>setNumber(number+1)
const substract=()=>setNumber(number+-1)
const multiplyBy2=()=>setNumber(number*2)

const combine() =>{
	multiplyBy2()
    add()
}

combine()을 실행하면 number*2+1가 아닌 number+1가 결과 값으로 나온다. 이유는 setNumber 비동기로 실행되기 떄문에 맨 마지막에 실행된 값이 실행 된것이다. 

number*2 실행중..-> number+1 실행중 ->(number*2완료) -> number+1 완료

이때 number은 multiplyBy2로 *2를 하더라도 number+1에서 받은 number의 값은 그대로이기 때문에 +1만 계산된다

개선책

const add=(number)=>setNumber(number+1)
const substract=(number)=>setNumber(number+-1)
const multiplyBy2=(number)=>setNumber(number*2)

const combine() =>{
	multiplyBy2()
    add()
}

number를 변수로 넣어주면 된다.

 

state의 시점


setState with setInterval

const [date, setDate] = useState(new Date())

window.setInterval(()=>{
	setDate(new Date())
}, 1000)

//setState가 일어나면 render가 다시 일어난다. 즉, setInterval이 또다시 실행된다.
//즉 컴포넌트가 처음 랜덜이 될 때만 mount를 써야한다

 

위의 방법은, setInterval이 무한 반복하므로, 처음 랜더링 될 때 setInterval, 언마운트될 때 clearInterval이 필요하다

이미지 출처 : https//projects.wojtekmaj.pl

이에 대한 해결을 하기 위해서 mount, unmount, update 시점의 제어는 함수형과 클래스형이 각각 다른방식으로 (그냥 모양만 다르다) 해결된다.

1. Class

export default class Clock extends React.Component{
	constructor(props){
    	super(props)
        this.state = {date:new Date()}
    }
    this(){this.setState({date:new Date()})}
	componentDidMount(){
    	this.timerID = setInterval(()=> this.tick(), 1000)
    }
    componentDidUpdate(){
    	console.log(this.state.date)
    }
    componentWillUnmount(){
    	clearInterval(this.timerID)
    }
}

2. Function

export default function App(){
	useEffect(()=>{
    	return()=>{clean}//지워야할 코드를 넣는다 componentWillUnmount
    }, [])//처음에만 실행한다
	useEffect(()=>{
     	return()=>{clean}//지워야할 코드를 넣는다 componentWillUnmount
    })//모든 re-rendering되었을 때 실행할 때만 사용. 모든 렌더링에 실행된다
	useEffect(()=>{
     	return()=>{clean}//지워야할 코드를 넣는다 componentWillUnmount
    }, ["A"])//처음에 실행한다 + A가 변경될 때 실행한다. 이를 의존성 배열이라한다
}

 

해결된 코드

useEffect(()=> {
	console.log("componentDidMount")
	const timerID = setInterval(tick, 1000)
    
    return() =>{
	    console.log("componentWillUnMount")
    	clearInterval(timerID)
    }
},[])
useEffect(()=> {
	console.log("componentDidMount")
	console.log(date)
},[date])
//따로 분리를 하는 이유는 date마다 업데이트가 되면,
//setInterval을 여러번 로드해야하기 때문에 따로 분리한다

 

 

Array.prototype.map으로 다량 컴포넌트 렌더링


const cardInfos = [
{
title:"",
imgSrc="",
author="",
},
{
title:"",
imgSrc="",
author="",
},
{
title:"",
imgSrc="",
author="",
}]

return({cardInfos.map((info)=>(
        <PreviewCard
        imgSrc={info.imgSrc}
        title={info.title}
        author={info.author}/>
    )
  )}
)

 

다량의 컴포넌트를 처리할 때, 주의사항!!

렌더링 과정에서 항목들을 고유하기 시별하기 위해서 다량의 컴포넌트를 처리할 때는 고유한 값이 들어있는 key를 넣어 주어야한다. 여기서 주의 할 것은 index를 넣으면 부정적인 영향이 있을 수 있으니 최후의 수단으로 index를 넣을 것  

위의 예시로 치면 <PreviewCard key={info.title} imgsrc={info.imgSrc}> 와 같이

return({cardInfos.map((info)=>(
        <PreviewCard
        imgSrc={info.imgSrc}
        title={info.title}
        author={info.author}
        key = {info.title}
        />
    )
  )}
)

합성 이벤트와 이벤트 헨들링


stopPropagation() :stopPropagation 은 부모태그로의 이벤트 전파를 stop 중지하라는 의미

preventDefault() :preventDefault 는 a 태그 처럼 클릭 이벤트 외에 별도의 브라우저 행동을 막기 위해 사용

const closeBanner =(e) =>{
	e.stopPropagation(); //closebutton을 누르면 closeBanner가 열리는 
    					 //이벤트 버블링을 막기위해 사용
    setvisible(false);
}

Form : <input>, <textarea>, <select>


Form으로 setState에서 값을 받기!(다른방법도 있다)

export default function App(){
	const [info, setInfo] = useState({
    	nickname:"",
        password:""
    })
	const handleChange = (e) =>{
		setInfo({...info,[e.target.name]:e.target.value })
    }//e.target.name은 computed property name을 사용하기 때문이다.
    const handleSubmit =(e)=>{
    	e.preventDefault();
        const {nickname, password} = info
        alert(`nickname:${nickname}, password:${password}`)
    }
	return(
    	<form onSubmit={handleSubmit}>
    		<input type="text" name="nickname" onChange={handleChange}/>
    	</form>
    )
}

 

비제어 컴포넌트


지금까지 사용해왔었던 useState같은 경우 제어 컴포넌트로, 리렌더를 유발합니다.

Ref import 하는법

//Ref import 하는법
//class component
import {createRef} from "react"
//function component
import {useRef} from "react"
export default function App(){
	const inputRef = useRef();
	const handleChange = (e) =>{
		setInfo({...info,[e.target.name]:e.target.value })
    }//e.target.name은 computed property name을 사용하기 때문이다.
    const handleSubmit =(e)=>{
		e.preventDefault();
		alert(inputRef.current.value)
		inputRef.current.focus();
    }
	return(
    	<form onSubmit={handleSubmit}>
    		<input type="text" name="nickname" ref={inputRef}/>
    	</form>
    )
}

ref ={inputRef}에 있는 값은, inputRef.current.value로 불러올 수 있다.

inputRef.current.fucus()는, input이 focus되게 만든다

<button onClick={()=>alert(catRef.current.height)}>

ref.current.height와 같이, ref된 대상의 값을 읽을 수도 있다.

ref로 받는다면, 리 랜더링이 안되기 때문에 바로 바로 값의 변화를 볼 수 없다. 

If useState로 값을 받는다면?

import React, {forwardRef, useEffect, useState} from "react"
export default function Parent(){
	const [height, setHeight] = useState(0)
	const callbackRef = (node) =>{
    if (node!==null){
    	setHeight(node.getBoundingClientRect().height	
    }
    }
    return (
    	<Child ref={callbbackRef}/>
    )
}
const child = forwardRef((props, ref)=>{
	const [loading, setLoaded] = useState(false)
	return(
    <img src="imgURL"
    	onLoad={()=>{setLoaded(true)}}
        ref ={loaded ? ref : () =>undefined}
        style={{width:"150px"}}
    /> 
    )
})

useRef를 사용할 때 처럼, 리랜더링이 안되는 문제는 없지만, 로드를 하는 과정에서 img가 아직 로드가 되지 않은 경우에는 height가 default값인 0이 나올 수 있기 때문에 loaded를 이용하여, 로드가 된 걸 state로 확인하고, ref 적용을 하는 것이 좋다.

컴포넌트에서 다른 컴포넌트 담기


function Child(props){
	return (
    	<div className={'FancyBorder FancyBorder-'+props.color}>
        	{props.children}
                //<h1>
                //Welcome
                //</h1>
                //<p>
                //Thank you
                //</p>
                //아래에 <Child/>안에 있는 것들이 props.children
		</div>
        
    )
}
function Parent(){
	return(
    	<Child color="blue">
        	<h1>
            	Welcome
            </h1>
            <p>
            	Thank you
            </p>
        </Child>
    )
}

 

React css


//1
<div style={{height:10}}> </div>
//2
<div style={{color:item.stocked? "black" : "red"}} </div>
//3
<div className="cn"> </div>
//4
let className = 'menu'
if(this.props.isActive){
	className += ' menu-active'
}
return <span className={className}> Menu </span>
//5
//css module
//6
//styled-components

 

1. div에 style을 직접 넣는 방법

2. 1번과 같은 방법이지만 조건문을 넣었다

3. className을 넣어서 해결하는 방법

4. 조건문을 넣어 className을 바꾸는 방법

5. css module은 내용이 길어서 아래에 따로 배치하였다 

//Button.module.css
.error{
	background-color:red
}
//another-stylesheet.css
.error{
	color:red
}
//Button.js

import React, {component} from 'react'
import styles from './Button.module.css'//module stylesheet as styles
import './another-stylesheet.css' //import regular stylesheet

render(
	return <button className ={styles.error}> error button </button>
)

module로 불러들인 경우에는, 원하는 케이스에만 적용할 수 있다는 장점이 있다.

6. styled components

//npm i install styled-components
//Product.style.js
import styled from "styled-components"

export const Category = styled.td`
	font-weight:bold
`

//App.js
import React from 'react'
import * as S from './Product.style.js'
export default function App(prop){
	return(
    	<S.Category> {category} </S.Category>
    )
}

6번째는 설치를 해야하지만, style.js내에서 css와 유사하게 문법을 사용하기 때문에 편리하다.

 

 

SPA(Single Page Application)와 React Router


SPA : 하나의 HTML 페이지와 어플리케이션에 실행에 필요한 JS, CSS를 로드하는 어플리케이션이다. 즉 후속페이지의 상호작용을 할 때 새로운 페이지를 불러오지 않기 떄문에 페이지가 다시 로드되지 않는다.

전통적인 웹 어플리케이션의 단점

1. 서버의 부담이 크다

2. 속도가 느려질 수 있다

3. 페이지 이동 시, 깜빡임이 발생한다.

url을 이동하더라도 새로운 HTML을 서버에서 받지 말고 화면을 Client에서 그리자! Client Side Rendering

SPA로 웹어플리케이션을 구축할 때, HTML은 하나지만, 유저가 볼 수 있는 화면은 여러개여야 한다.

라우팅 : URL에 따라 알맞는 콘텐츠(UI)를 전달해주는 기능

 

import {BrowserRouter, Routes, Route, Outlet, useNavigate, useParams} from "react-router-dom"
import MainPage from"./components/MainPage"
import BlogPage from"./components/BlogPage"
import TechPage from"./components/TechPage"

function App(){
	const navigate= useNavigate()
	return(
    	<BrowserRouter>
       		<h5 onClick ={()=>navigate("/")}> logo</h5>//useNavigate()는 Router안에서 사용해야 한다
        	<Routes>
            	<Route path={"/"} element={<MainPage/>}/>
                <Route path={"/tech"} element={<TechPage/>}>
                	<Route path="javascript" element={<JavascriptPage>}/>
                    <Route path="react" element ={<ReactPage>}>
                    	<Route path=":docId" element={<ReactDocPage/>}/>
                    </Route>
                </Route>
                <Route index element={<Activity/>}/>//index는 invoices activity도 안걸릴때 index로 이동
                <Route path={"*"} element={<NotFound/>}/>
				<Route path={"/blog"} element={<BlogPage/>}/>
            </Routes>
        </BrowserRouter>
    )
}

export default function TechPage(){
	return (
    	<div>
        	<Link to="/blog">Blog</Link>
            <Link to="/tech">Main</Link>
        </div>
        <Outlet/> // Route안의 Route를 사용하기 위해선 꼭 필요하다
    )
}
export default function ReactPage(){
	return(
    	{docs.map((doc)=>(
        	<Link
        	to={`${doc.id}`}
            key={doc.id}>
            {doc.id}
            <Outlet/>
            </Link>
        ))}
    )
}
export default function ReactDocPage(){
	const params = useParams()
    return(
		<div> `${docID}` </div>
    )
}

ReactPage에서 Outlet을 제외하고, ReactDocPage 라우터에 링크를 react/:docID로 한다면, 새로운 페이지가 생기고

ReactPage에 Outlet을 넣고 ReactDocPage 라우터에 링크를 :docId로 한다면 detail summary 구조로 모양이 나온다.

 

비동기 방식 실행 


Fetch :

useEffect(()=>{
	async function fetchData(){
    	const res = await fetch("https://codevil.tistory.com")
        const result = await res.json()
        setDocs(result)
    }
    fetchData().then((res)=>{
    	setDocs(res)
    })
},[])

SWR :

사용자가 페이지를 탐색 중 다른 탭을 보다가 다시 돌아왔을 때나 혹은 네트워크가 끊어졌다가 다시 연결되었을 때 refetch를 할 수 있도록 옵션으로 설정할 수 있습니다

 

 

import useSWR from "swr";

function Profile() {
  const { data:docs, error } = useSWR("/api/user", fetcher, options);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

 

Global State


웹 어플리케이션을 개발하다보면, 전역으로 관리해야하는 값들이 있다.

1. useContext+ createContext+useState 이용한 전역 상태 관리

import React, {createContext, useState, useContext} from "react"
import Blog from "./BlogPage"
export const UserContext = createContext()

export default function UserStore(props){
	const [job, setjob] = useState("FE dev")
    const user ={
    	name:"the name".
        job : "FE dev",
        changeJob : (updatedJob) =>setJob(updatedJob),
    }
    return(
    	<UserContext.Provider value={user}>
        	{props.children}
        </UserContext.Provider>
    )
}

function App(){
	<UserStore>
		<BlogPage/>
	</UserStore>
}
//blog.js
import React, {useContext} from "react"
import {UserContext} from "./App"
export default function BlogPage(){
	const user= useContext(UserContext)
    return(
    	<div>{user.name}</div>
    )
}

Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider value props가 바뀔 때마다 다시 렌더링 되는 것이 특징으로, Redux와의 차이점이다.

2. useContext+ createContext+useReducer

다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 state가 이전 state에 의존적인 경우에 useState보다는 useReducer를 더 선호합니다. 또, useReducer는 콜백 대신 dispatch를 전달가능하다.

//user.js
import React, {useReducer, createContext} from "react"
	export const UserContext = createContext()
	const initialUser ={
    	name:"the name".
        job : "FE dev",
    }
    const userReducer = (state, action) =>{
    	switch (action.type){
        	case "changeJob":
            	return {...state, job:action.text}
            break;
            default :
            break;
        }
    }
export default function UserStore(props){
	const [user, dispatch] = useReducer(userReducer)
    return(
    	<UserContext.Provider value={dispatch}>
        	{props.children}
        </UserContext.Provider>
    )
}

function App(){
	<UserStore>
		<BlogPage/>
	</UserStore>
}
//blog.js
import React, {useContext} from "react"
import {UserContext} from "./App"
export default function BlogPage(){
	const dispatch= useContext(UserContext)
    return(
    	<div>
        {user.name}
        <button onClick = {()=>dispatch({
        	type:"changeJob", text:"BE dev"
        })}
        Change Job
        </button>
        </div>
    )
}

3. Redux

context API + {useState || useReducer} vs Redux

성능적인 측면에서는 Redux가 좋다

Context API의 단점은 value = {a:b, c:d} 어떤 값이든 값이 바뀌면 값의 사용 유무와 상관없이 리랜더링된다.

이를 피하기 위해 Context는 둘로 쪼개는 상황이 나온다. 성능을 개선하기 위해서 이런 번거운일을 해결해야한다.

 

Redux 처리과정


1. 컴포넌트의 useEffect 내에서 API 호출하고, 응답 받은 결과를 스토어에 업데이트한다.

dispatch({ type:"updateUser", payload:{nickname:"김코딩", purchased: "패키지"}})

2. 컴포넌트의 useEffect 내에서 dispatch ({type:"updateUser"})로 액션 객체만을 보내고, user Store의 reducer안에서 API를 호출하고, 응답 받은 결과를 스토어에 업데이트한다

>Reducer는 순수함수이기 때문이다.

 

Action 객체를 dispatch ->middleware ->Reducer -> Store 업데이트

 

비동기 통신을 위한 Redux-middleware


redux-thunk : dispatch에 action 객체가 아닌 thunk 함수를 전달한다 (easy)

redux-saga : generator를 활용한다. (medium)

redux-observable : RxJs를 기반으로 한다 (hard)

 

Redux와 관련된 것은, 공식 문서를 참고해서 진행을 하도록하자! 지금 까지 나왔던 것들 과는 다르게 파일이나 다루는 게 한개의 글로 표현하기엔 복잡하다! (아마 나중에 다른 Redux관련 글을 쓰고 링크를 거는게 나을꺼같다!)

 

그 외 여러가지 상태관리 도구들


Recoil : 리액트 팀에서 직접 만든 상태 관련 라이브러리. 비동기 데이터 통신을 위한 기능 제공. React 내부에 접근이 가능하며 동시성 모드, Suspense 등을 지원

Jotai : Recoil에 영향을 받아 일본에서 만들어진 라이브러리

Constate : React Context + State, Context의 단점을 개선

비동기 통신과 전역 상태 관리의 최신 흐름


API 호출하기 - 전역 상태 관리하기 - API 호출하고 전역 상태에 update 하기

: 이런 API 호출부터 전역상태를 update하는 것이 복잡하다

1. API 호출 후 응답하는 데이터는 Server State

2. UI 개발을 위한 데이터는 Client State

서버에 Store를 개발하다보면 1과 2가 섞이는 경우가 생길 수 있다. 따라서, react query를 이용하여 Sever 데이터와 Client데이터를 분리할 수 있다.

 

React Query


api.js

import axios from "axios";

export const getUser = () => {
  return axios.get("/user").then((res) => res.data);
};
  
export const updateNickname = (nickname) => {
  return axios.put(`/update-nickname?nickname=${nickname}`);
};
export const getPosts = () => {
  return axios.get("/posts").then((res) => res.data);
};

 

 

handlers.js

import { rest } from "msw";
//rest get이 답을 가로채서 이 응답을 내려준다.
import { db } from "./db";

export const handlers = [
  rest.get("/user", (req, res, ctx) => {
    return new Promise((resolve) =>
      setTimeout(() => {
        return resolve(res(ctx.status(200), ctx.json(db.user.getAll()[0])));
      }, 3000)
    );
  }),

  rest.put("/update-nickname", (req, res, ctx) => {
    const nickname = req.url.searchParams.get("nickname");
    const updated = db.user.update({
      where: { id: { equals: 1 } },
      data: { nickName: nickname },
    });

    return res(ctx.json(updated));
  }),
  rest.get("/posts", (req, res, ctx) => {
    return new Promise((resolve) =>
      setTimeout(() => {
        return resolve(
          res(
            ctx.status(200),
            ctx.json([{ title: "test1" }, { title: "test2" }])
          )
        );
      }, 5000)
    );
  }),
];

db.js

import { factory, primaryKey } from "@mswjs/data";

export const db = factory({
  user: {
    id: primaryKey(),
    nickName: "",
    email: "",
  },
});

db.user.create({
  id: 1,
  nickName: "hwarari",
  email: "hwarari@gmail.com",
});

App.js

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Edit from "./pages/Edit";
import { QueryClientProvider, QueryClient } from "react-query";
import { Suspense } from "react";
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
    },
  },
});
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<span>Loading </span>}>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/edit" element={<Edit />} />
          </Routes>
        </BrowserRouter>
      </Suspense>
    </QueryClientProvider>
  );
}

export default App;

Edit.js

import React, { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "react-query";
//userQuery를 쓰려면 유니크 키와, API호출을 할 수 있는 함수
import { getUser, updateNickname, getPosts } from "../mocks/api";

export default function Edit() {
  const [inputValue, setInputValue] = useState("");
  const queryClient = useQueryClient();
  const { data: user } = useQuery("@getUser", getUser, { staleTime: Infinity });
  const { data: posts } = useQuery("@getPosts", getPosts, {
    staleTime: Infinity,
  });
  const mutation = useMutation(updateNickname, {
    onSuccess: () => {
      queryClient.invalidateQueries("@getUser");
    },
  });

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate(inputValue);
  };

  return (
    <>
      <h1>Edit</h1>
      <h3>현재 닉네임: {user.nickName}</h3>
      <form onSubmit={handleSubmit}>
        <label>
          변경할 닉네임:
          <input type="text" value={inputValue} onChange={handleChange} />
        </label>
      </form>
      <ul>
        {posts.map((post) => (
          <li>{post.title}</li>
        ))}
      </ul>
    </>
  );
}

여기서 Suspense는 기다리는동안 잠시 로딩상태로 만드는 기능이다.

그리고 useQuery를 사용하면 직렬, useQuries를 사용하면 병렬적으로 실행하며 아직 useQuries를 사용하면 약간의 에러가 있다!

렌더링 성능 최적화 - useMemo, useCallback


 컴포넌트는 자신의 state가 변경되거나 부모에게서 받은 props가 변경 되었을때 다시 렌더링된다

심지어 자식 컴포넌트에서 렌더링 최적화를 별도의 코드로 추가하지 않으면, 부모에게서 받은 props가 변경되지 않더라도 다시 리렌더링된다.

useMemo 예시

https://codesandbox.io/s/frosty-pine-nvhewx?file=/src/Info.js  - 출처

 

frosty-pine-nvhewx - CodeSandbox

frosty-pine-nvhewx by leehwarang using react, react-dom, react-scripts

codesandbox.io

info.js
import React, { useMemo } from "react";
import "./styles.css";

const getColorKor = (color) => {
  console.log("getColorKor");
  switch (color) {
    case "red":
      return "빨강";
    case "orange":
      return "주황";
    case "yellow":
      return "노랑";
    case "green":
      return "초록";
    case "blue":
      return "파랑";
    case "navy":
      return "남";
    case "purple":
      return "보라";
    default:
      return "레인보우";
  }
};

const getMovieGenreKor = (movie) => {
  console.log("getMovieGenreKor");
  switch (movie) {
    case "Marriage Story":
      return "드라마";
    case "The Fast And The Furious":
      return "액션";
    case "Avengers":
      return "슈퍼히어로";
    default:
      return "아직 잘 모름";
  }
};

const Info = ({ color, movie }) => {
  const colorKor = useMemo(() => getColorKor(color), [color]);
  const movieGenreKor = useMemo(() => getMovieGenreKor(movie), [movie]);

  return (
    <div className="info-wrapper">
      제가 가장 좋아하는 색은 {colorKor} 이고, <br />
      즐겨보는 영화 장르는 {movieGenreKor} 입니다.
    </div>
  );
};

export default Info;

위의 예시는 useMemo를 이용하였다. Info의 윗부분이 만일 아래와 같이 구성되어 있다고 할 때와 비교하면

const Info = ({ color, movie }) => {
  const colorKor = getColorKor(color);
  const movieGenreKor = getMovieGenerKor(movie);  
}

useMemo를 사용하기전에는 color가 바뀔때 movie도 같이 리랜더가 된다. 하지만 useMemo를 사용하고 나면 변경한 값들만 리랜더가된다.

 

 

useMemo를 사용하면 props 값이 변경되지 않는한 다시 실행되지 않는다

useCallback을 사용하면 의존하는 값들이 바뀌지 않는한 계속해서 반환한다.

 

Code splitting - Dynamic import, React.lazy()


개발하는 앱이 커지면 번들이 점점 무거워지기 때문에, 성능상의 이슈가 생긴다. 이 번들을 가볍게하려면 코드분할을 해야하는데, 이것을 하는 방법은

1. 런타임에 여러 번들을 동적으로 만들어 와서 사용

2. 지연 로딩을 사용

하는 두가지 방법이있다.

1. 동적 번들같은 경우에는 babel, webpack으로 구현할 수 있고

2. 지연로딩같은 경우에는 lazy 함수를 이용해서 구현이 가능하다.

const Post = lazy(() => import("./Post"))

 

React SEO 개선 방법


구글을 제외한 검색 엔진은 자바스크립트를 실행하지 않고 진행하기 때문에 SEO에 불리하다. 따라서, Server Side Rendering을 이용하여 JS코드를 HTML로 변환해서 서버가 내려주는 방식을 택한다.

즉, 브라우저가 JS코드를 실행하지 않아도 화면을 그릴 수 있다. ReactDOMServer를 이용해서!