[React] chap.3

 

 

 

Build
// npm
npm run build

// yarn
yarn build

build 폴더 안에 컴파일된 파일 생성.

해당 파일은 리액트로 개발한 소스들과 JSX 파일 등을 브라우저가 읽을 수 있는 html, css, js로 컴파일한 파일들이 모여있다.

 

- 하위 경로에 배포

package.json 파일의 큰 object에 경로 추가

"homepage" : "http://url.com/blog",

 

 

 

Context API

컴포넌트가 여러개 중첩되어 있을 경우 props의 중복사용 하는것을 대체로 사용한다.

 

1. context 생성

let 재고context = React.createContext();

function App(){
  let [재고, 재고변경] = useState([10,11,12]);

  return (
    <HTML많은곳/>
  )
}

 

2. context 범위 지정

let 재고context = React.createContext();

function App(){
  let [재고, 재고변경] = useState([10,11,12]);

  return (
    <HTML많은곳/>
    <재고context.Provider value={재고}>
      <카드레이아웃3개생성하는부분/>
    </재고context.Provider>
    
  )
}

 

3. context 불러오기

import React, {useState, useContext} from 'react';

function Card(){
  let 재고 = useContext(재고context);

  return (
    <HTML많은곳/>
    <div>{재고[0]}</div>
  )
}

 

 

** 컴포넌트가 다른 파일에 있을 때

export let 재고context = React.createContext();

function App(){
  let [재고, 재고변경] = useState([10,11,12]);

  return (
    <HTML많은곳/>
    <재고context.Provider value={재고}>
      <Detail/>
    </재고context.Provider>
  )
}

- context를 export한다. 

 

import {재고context} from './App.js';

function Detail(){
  let 재고 = useContext(재고context);

  return (
    <HTML많은곳/>
  )
}

 

 

 

애니메이션
// npm
npm install react-transition-group

// yarn
yarn add react-transition-group
import {CSSTransition} from 'react-transition-group';

 

 

function Detail(){
  return (
    <div>
      <CSSTransition in={true} classNames="wow" timeout={500}>
        <TabContent 누른탭={누른탭} />
      </CSSTransition>
    </div>
  )
}

function TabContent(){
  if (props.누른탭 === 0){
    return <div>내용0</div>
  } else if (props.누른탭 === 1){
    return <div>내용1</div>
  } else if (props.누른탭 === 2){
    return <div>내용2</div>
  }
}
.wow-enter {
  opacity : 0
}

.wow-enter-active {
  opacity : 1;
  transition : all 500ms;
}

 

 

 

 

Redux

1. props 전송 없이도 모든 컴포넌트 들이 state를 사용할 수 있게 만들어준다.

// npm
npm install redux react-redux

//yarn
yarn add redux react-redux

(redux, react-redux 두 개)

 

 

- 세팅

(index.js)

import 많은곳;
import {Provider} from 'react-redux';

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider>
        <App/>
      </Provider>
    </BrowserRouter>
  </React.StrictMode>

);

1. Provider import

2. state 공유를 원하는 컴포넌트 감싸기

 

(index.js)

import 많은곳;
import {Provider} from 'react-redux';
import {createStore} from 'redux';

let store = createStore(()=>{ return [{id : 0, name : '멋진신발', quan : 2}]  })

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider>
        <App/>
      </Provider>
    </BrowserRouter>
  </React.StrictMode>

);

3. createStore() 함수로 redux에서 state 만들기

 

(index.js)

import 많은곳;
import {Provider} from 'react-redux';
import {createStore} from 'redux';

let store = createStore(()=>{ return [{id : 0, name : '멋진신발', quan : 2}]  })

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App/>
      </Provider>
    </BrowserRouter>
  </React.StrictMode>

4. Provider에 만든 state를 props처럼 등록하기

 

 

- redux로 등록한 데이터 쓰기

(Cart.js)

import 많은곳;
import {connect} from 'react-redux';
function Cart(props){
  return (
    <div>
      <Table responsive>
        <tr>
          <th>#</th>
          <th>상품명</th>
          <th>수량</th>
          <th>변경</th>
        </tr>
        <tr>
          <td>1</td>
          <td>{props.state[0].name}</td>
          <td>Table cell</td>
          <td>Table cell</td>
        </tr>
      </Table>
    </div>
  )
}

function state를props화(state){
  return {
    state : state
  }
}

export default connect(state를props화)(Cart);

1. connect함수를 import하고 아래에 function을 하나 만든다.

2. export default 부분에 import한 connect를 사용한다.

 

밑에 생성한 함수는 Cart라는 함수에서 props로 받아서 쓸 수 있도록 state를 props화 하는 역할의 함수이다.

 

 

2. state(상태) 관리에 용이하다.

createStore 함수로 만들었던 object가 있는 index.js에 수정에 용이한 함수를 만든다. (데이터의 수정방법을 미리 정의해둘 수 있다.)

 

- reducer

(index.js)

let 기본state = [{id : 0, name : '멋진신발', quan : 2}];

function reducer(state = 기본state, 액션){
  if (액션.type === '수량증가') {
    return 수량증가된새로운state
  } else {
    return state
  }
  
}
let store = createStore(reducer);

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store={store}>
        <App/>
      </Provider>
    </BrowserRouter>
  </React.StrictMode>
);

redux의 데이터 수정은 reducer의 액션(데이터수정방법) 이라는 파라미터의 type 값을 통해서 데이터의 변경이 일어날지 체크한다.

 

(Cart.js에 있던 버튼)

<button onClick={()=>{ props.dispatch({type: '수량증가'}) }}> + </button>

기본state라는 object가 있고 해당 object의 데이터의 수정이 일어난 것을 다른 하위 컴포넌트에서 받아온다. 꼭 수정할땐 props.dispatch()를 사용한다.

 

 

- reducer 여러개

* 이렇게 object의 API 스펙을 변경하면 나중에 문제가 생긴다. (상품만 꺼낼땐 true를 걸러야 하는데 추가적인 작업이 또 필요하다.)

let state초기값 = [ {id : 0, name : '새로운상품', quan : 2}, true ];

 

 

- reducer 여러개 정의

(index.js)

import {createStore, combineReducers} from 'redux';

let state초기값 = [ {id : 0, name : '새로운상품', quan : 2} ];

function reducer(){
  저번시간 만든 리듀서
}

let alert초기값 = true;

function reducer2(state = alert초기값, 액션){
  return state
}

let store = createStore( combineReducers({reducer, reducer2}) )

createStore함수 안에 combineReducers 함수를 쓰고 안에 각 데이터를 object 형태로 파라미터를 넘겨주면 API 스펙 변경없이 만든 reducer들을 추가할 수 있다. (combineReducers import 필요)

 

 

- reducer 여러개 사용

(Cart.js)

function Cart(props){
  return (
    <div>
      <Table>table부분</Table>
      { props.alert열렸니 === true
       ? (<div className="my-alert2">
            <p>지금 구매하시면 20% 할인</p>
            <button>닫기</button>
          </div> )
       : null 
       }
    </div>
  )
}

function state를props화(state){
  console.log(state); // {reducer: [], reducer2: true}
  return {
    state : state.reducer,
    alert열렸니 : state.reducer2 //리듀서2에 있는거 가져오는법
  }
}
export default connect(state를props화)(Cart);

reducer를 store에 저장하는 부분이 바뀌면 reducer 사용하는 부분의 데이터 형식도 바뀌게 된다. 때문에 받아오는 부분도 수정이 필요하다.

 

 

- dispatch의 추가 기능

import {connect} from 'react-redux'

(Detail.js)

function Detail(props){
  return (
    <HTML많은곳/>
    <button onClick={()=>{ 
  
      props.dispatch({type : '항목추가', payload : {id : 2, name : '새로운상품', quan : 1} }) 

    }}>주문하기</button>
  )
}

function state를props화(state){
   ...
   ...
}
export default connect(state를props화)(Detail)

dispatch함수의 payload 데이터를 보면 redux store까지 해당 데이터를 실어 보낼 수 있고 reducer안에서 보낸 데이터를 사용할 수 있다. (새로운 js에서 redux를 사용하면 connect import와 props로 사용할 수 있는 함수를 정의해야한다)

 

(index.js)

let 기본state = [{id : 0, name : '멋진신발', quan : 2}];

function reducer(state = 기본state, 액션){

  if (액션.type === '항목추가') {

    let copy = [...state];
    copy.push(액션.payload);
    return copy;
    
    } else {
      ...
      ...
    }
}

reducer(state, '액션') {} 에서 액션이라는 파라미터는 dispatch 함수 안에 들어있던 모든게 들어있다. 

 

 

데이터를 저장하고 수정할 때 react는 SPA이기 때문에 개발단계에서 페이지 이동이 일어날 시에 state 데이터가 초기화 된다. 데이터가 변경된 것을 확인하고 싶으면 라우터 함수(history.push 등)를 사용해서 결과를 나타내는 페이지를 강제로 호출하면 변경된 데이터를 확인할 수 있다.

 

 

* useSelector

redux store에 있는 state를 꺼내쓰려면 props화 해주는 함수를 첨부해야하는데 그 때 useSelector Hook을 사용하면 좀 더 쉽게 꺼내올 수 있다.

// function state를props화(state){
//     // console.log(state);
//     return {
//         state: state.reducer,
//         alert열렸니: state.reducer2
//     }
// }

// export default connect(state를props화)(Cart)
export default Cart;

- props화 함수 삭제하고 기존 export 주석해제

 

import { useSelector } from 'react-redux';

function Cart(props) {
  let state = useSelector((state) => state )
  console.log(state.reducer)
  
  (생략)
}
<tbody>
  {
   state.reducer.map((a,i)=>{
     return (
      <tr key={i}>
      <td>{ a.id }</td>
      <td>{ a.name }</td>
      <td>{ a.quan }</td>

- useSelector import 및 기존 (props.state => state.reducer 변경)

 

 

* useDispatch

props화 하는 함수말고도 dispatch함수도 간단하게 사용 가능하다.

import { useSelector, useDispatch } from 'react-redux';

function Cart(props) {
  let state = useSelector((state) => state )
  let dispatch = useDispatch()
  
  (생략)
}
<td><button className="btn btn-primary" onClick={() => {
    dispatch({ type: '수량증가', payload: { status: {i} } })
}}>+</button>

- useDispatch import 하고 변수로 선언하여 사용 (props.dispatch() => dispatch() 변경)

 

 

 

react if 응용

- if / else

function Component() {
  if ( true ) {
    return <p>참이면 보여줄 HTML</p>;
  } else {
    return null;
  }
}

return()안에서의 if는 쓸 수없다. if는 return을 반환 할때 사용한다.

 

 

- 삼항연산자(ternary operator)

function Component() {
  return (
    <div>
      {
        1 === 1
        ? <p>참이면 보여줄 HTML</p>
        : null
      }
    </div>
  )
}

return()안에서의 if는 쓸 수없으니 삼항 연산자로 대체 할 수 있다.

 

 

- 오브젝트 자료형을 응용한 enum

var 탭UI = { 
  info : <p>상품정보</p>,
  shipping : <p>배송관련</p>,
  refund : <p>환불약관</p>
}

function Component() {
  var 현재상태 = 'info';
  return (
    <div>
      {
        탭UI[현재상태]
      }
    </div>
  )
}

자바스크립트 오브젝트 자료형을 응용하여 if문처럼 사용할 수 있다. (var 변수 대신 state에 object형으로 넣어 사용할 수 있다)

 

 

 

state변경함수 주의: async

(2022년 이후 리액트 18.0버전 batching 업데이트 전까지)

import React, { useState } from "react";

function Test(){

    let [count, setCount] = useState(0);
    let [age, setAge] = useState(20);

    return(
        <div>
            <div>안녕하십니까 전 {age}</div>
            <button onClick={() => {
                /***** 22까지만 증가, 23부터는 그대로 *****/
                { count < 3
                    ? setAge(age + 1)
                    : setAge(age);
                }                
                setCount(count + 1)
            }}>누르면 한살먹기</button>
        </div>
    );
}

export default Test;

결과: 23까지 올라감

 

이유는 state변경 함수의 특징이 async로 작동하기 때문이다.

setCount(count + 1)에서 3으로 만들 때, 이 작업이 async로 동작하게 되어 count의 값을 3으로 만들기 전에 삼항연산자에 count < 3의 작업을 먼저 실행하는데 그 시점의 count의 값은 아직 2이기 때문에 age에 +1을 한다.

 

결국 state1을 변경하고 state2를 변경하는 코드를 작성할땐 가끔 문제가 생긴다. 때문에 sync처럼 실행시키기위해선 useEffect를 사용하면된다.

import React, { useEffect, useState } from "react";

function Test(){

    let [count, setCount] = useState(0);
    let [age, setAge] = useState(20);
    
    useEffect(() => {
        
        if(count != 0 && count < 3){ // 처음 로드 됐을 때 원치않은 값 증가 방지
            setAge(age + 1)
        }else{
            setAge(age)
        }        
        console.log(count);
    }, [count])

    return(
        <div>
            <div>안녕하십니까 전 {age}</div>
            <button onClick={() => {
                setCount(count + 1)
                // { count < 3
                //     ? setAge(age + 1)
                //     : setAge(age);
                // }                
            }}>누르면 한살먹기</button>
        </div>
    );
}

export default Test;

useEffect를 사용해서 count가 변하면 age+1을 sync하게 변경했다. 그러면 원래 의도대로 값을 증가 시킬 수 있는데 문제는, useEffect가 처음 로드될 때도 한번 실행이 되기 때문에 useEffect를 막는 코드를 사용해도 되고 if문에 조건을 하나 더 추가한다.

 

꼭 useEffect말고도

 

1. count와 age를 동시에 한 state에 array나 object 자료형으로 저장해도 되고

2. 바뀔때마다 렌더링이 필요한 변수는 state로 만들고 그럴 필요 없는건 let변수로 만들어서 사용해도 된다.

 

 

 

 

performance

1. 함수나 오브젝트는 변수에 담아쓰자 (메모리공간을 아낄 수 있다.)

var 스타일 = {color : 'red'};

function Cart(){
  return (
    <div style={ 스타일 } ></div>
  )
}

컴포넌트 바깥에 있는 변수에 저장해서 쓰자. ( => function() 밖 )

컴포는트가 재 렌더링 될 때 변수에 저장되지 않은 이름 없는 자료형들은 매번 새로운 메모리 영역을 할당해줘야하기 때문에 성능상 좋지 않다. (class는 class안에 함수 집어넣는 공간에 저장)

 

 

2. 애니메이션 줄 때 레이아웃 변경 애니메이션은 피하자

- css렌더링 단계에서 layout 영역의 스타일(width, margin, padding, left, right, top ,bottom 등) 말고 그것보다 가벼운 단계 (transform, opacity 등)과 같은 속성을 애니메이션으로 이용한다.

 

 

3. 컴포넌트 import할 때 lazy하게 import 한다.

컴포넌트 중에 메인페이지에 들어왔을 때, 첫 페이지 방문 시 필요하지 않은 컴포넌트들은 꼭 전부 로딩 할 필요가 없다.

 

 

* lazy loading

(App.js 상단)

import React, {useState, useContext, lazy, Suspense} from 'react';
let Detail = lazy( ()=>{ return import('./Detail.js') } );

(App.js 중단에 Detail 컴포넌트 쓰는 곳)
render (
  <Suspense fallback={ <div>로딩중입니다~!</div> }>
    <Detail/>
  </Suspense>
)
  • react 라이브러리에서 lazy, Suspense를 import한다.
  • lazy함수를 이용해서 lazy loading 할 페이지를 변수(Detail)로 만든다.
  • Suspense 컴포넌트로 해당 import한 변수(Detail)를 감싸준다.
  • fallback 속성엔 컴포넌트 로딩 전까지 띄울 HTML을 적어준다.

 

 

* React Dev Tools(리액트 개발자 도구 크롬 확장프로그램)

해당 크롬 확장 프로그램을 설치하면 개발자 도구에서 컴파일된 상태 이전에 리액트 버전의 컴포넌트들을 확인 할 수 있다.

- Components: 현재 페이지에 사용된 모든 컴포넌트와 props, state 확인 및 수정, 렌더링 조작 등

- Profiler: 렌더링 되는 속도 측정 및 상태 보기

 

**애드 블록이 설치되어있다면 비활성화 시켜야 동작 시킬 수 있다.**

 

 

* memo

컴포넌트는 컴포넌트와 관련된 state나 props 가 변경되면 자동 재 렌더링 되는데 의도하지 않은 컴포넌트 또한 재 렌더링 되는경우를 막고싶을 때 사용한다.

 

function Cart(){
  return (
    <Parent 이름="존박" 나이="20"/>
  )
}

function Parent(props){
  return (
    <div>
      <Child1 이름={props.존박} />
      <Child2 나이={props.나이} />
    </div>
  )
}

function Child1(){
  useEffect( ()=>{ console.log('렌더링됨1') } );
  return <div>1111</div>
}
function Child2(){
  useEffect( ()=>{ console.log('렌더링됨2') } );
  return <div>2222</div>
}

여기서 Parent컴포넌트에 있는 이름 값이 바뀌면 Child1의 값만 재렌더링 되는것이 아니라 Child2의 값 또한 재 렌더링 된다. (Parent를 구성하는 state나 props가 변경되면 관련된 모든 컴포넌트가 다 재 렌더링 되기때문)

 

 

import React, {useEffect, memo} from 'react';

function Cart(){
  return (
    <Parent 이름="존박" 나이="20"/>
  )
}

function Parent(props){
  return (
    <div>
      <Child1 이름={props.존박}/>
      <Child2 나이={props.나이}/>
    </div>
  )
}
function Child1(){
  useEffect( ()=>{ console.log('렌더링됨1') } );
  return <div>1111</div>
}
let Child2 = memo(function(){
  useEffect( ()=>{ console.log('렌더링됨2') } );
  return <div>2222</div>
})

1. react에서 memo를 import한다.

2. 원하는 컴포넌트(Child2)를 memo로 감싼다.

 

- memo의 동작은 memo로 감싼 컴포넌트를 아무렇게나 재 렌더링을 안시키려고 기존 props와 바뀐 props를 비교하는 연산이 추가로 진행되기 때문에 이 동작 자체로도 부담이 될 수 있다. 사용할지 말지 리액트 개발자 도구에서 렌더링속도를 측정해보고 작은 사이트거나 HTML의 양이 매우 적을 경우엔 안쓰는게 낫다.

 

 

 

PWA (Progressive Web App)

PWA는 웹사이트를 안드로이드/iOS 모바일 앱처럼 사용할 수 있게 만드는 웹개발 기술이다. react는 웹앱을 만드는 기술이라 모바일 App처럼 자연스럽게 동작하는 사이트를 만들 수 있는데 PWA를 사용하면 이 웹 사이트를 모바일 앱처럼 발행해서 쓸 수 있다.

 

장점:

1. 스마트폰, 태블릿 바탕화면에 웹사이트를 설치 가능하다.

2. 오프라인에서도 동작할 수 있다.

3. 설치 유도 비용이 매우적다. (앱설치를 유도하는 마케팅 비용 -> 플레이스토어에서 설치하고 다운받는 과정)

 

npx create-react-app 프로젝트명 --template cra-template-pwa

PWA를 사용하려면 프로젝트를 생성할 때, 이런 방식으로 생성해야한다. (기존 프로젝트에서 변경 못함) 때문에 새로운 프로젝트를 만든 후 파일들을 옮겨야한다. (* index.js는 바뀐점이 있어서 차이점을 보고 옮겨야 한다.) 물론 라이브러리도 전부 설치해야한다.

 

index.js를 보면

serviceWorkerRegistration.unregister();

이 부분을

serviceWorkerRegistration.register();

이렇게 수정하면 된다.

 

// npm
npm run build

// yarn
yarn build

그리고 그 후에 빌드한다.

 

 

* manifest.json

{
  "version" : "여러분앱의 버전.. 예를 들면 1.12 이런거",
  "short_name" : "설치후 앱런처나 바탕화면에 표시할 짧은 12자 이름",
  "name" : "기본이름",
  "icons" : { 여러가지 사이즈별 아이콘 이미지 경로 },
  "start_url" : "앱아이콘 눌렀을 시 보여줄 메인페이지 경로",
  "display" : "standalone 아니면 fullscreen",
  "background_color" : "앱 처음 실행시 잠깐 뜨는 splashscreen의 배경색",
  "theme_color" : "상단 탭색상 등 원하는 테마색상",
}

웹앱의 아이콘, 이름, 테마색 등을 결정하는 파일이다. 때문에,

 

<link rel="manifest" href="/manifest.webmanifest">

파일이 추가되어야 하지만 이 부분은 리액트가 알아서 해준다.

 

 

* service-worker.js

앱을 설치하고나면 앱 구동에 필요한 이미지, 데이터들이 전부 하드에 설치가 되는데 그렇게되면 앱을 켰을 때 서버에 요청해서 가져오는게 아니라 이미 하드에 설치되어있는걸 그대로 가져와서 쓴다.

 

service-worker 파일은 이런 앱의 특성을 비슷하게 도와주는 파일이다. 이 파일 때문에 웹앱을 설치했을 때, CSS, JS, HTML, IMG 파일 등이 하드에 설치되게 결정 하고 앱을 켤때마다 설치되어 있는 곳(Cache Storage)에 저장되어있는 파일을 사용하게 된다. (오프라인에서도 사용이 가능하게 된다) 또, 자주 변해서 저장하기 싫은 파일들은 제외 시킬 수 있다.

 

 

* PWA 커스터마이징

node_modules/react-scripts/config/webpack.config.js
// 구
new WorkboxWebpackPlugin.GenerateSW({
    clientsClaim: true,
    exclude: [/\.map$/, /asset-manifest\.json$/],
}) 

// 신
new WorkboxWebpackPlugin.InjectManifest({
    swSrc,
    dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
    exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],

exclude 부분에 정규식으로 작성하면 그 정규식과 일치하는 파일명은 제외한다.

 

 

 

storage

DB가 아닌 브라우저에 정보를 저장할 수 있는데 그 때 쓰는 것이 storage이다. (localStorage, sessionStorage 등)

- localStorage: 브라우저의 어떤 공간에 데이터를 저장 할 수 있다.

- sessionStorage: 브라우저가 종료되기전까지 데이터를 저장 할 수 있다.

localStorage.setItem('데이터이름', '데이터'); // item 저장
localStorage.getItem('데이터이름'); // item 가져오기
localStorage.removeItem('데이터이름') // item 삭제

// sessionStorage도 마찬가지

object 자료형처럼 key: value 형태로 자료를 저장할 수 있다.

 

 

* JSON

localStorage.setItem('obj', JSON.stringify({name:'kim'}) );

storage에 저장되는 값들은 단순 텍스트 자료만 저장하기때문에 object나 array 자료형을 그냥 저장하면 데이터가 깨지게 된다. 때문에 JSON 형태로 저장해야하는데 자바스크립트에선 JSON.stringify 함수를 사용하면 JSON 형태로 데이터를 변환하여 저장할 수 있다.

 

var a = localStorage.getItem('obj');
var b = JSON.parse(a)

반대로 해당 데이터를 다시 꺼내면 JSON데이터 그대로 나오기 때문에 해당 자료형으로 사용하려면 다시 원 자료형으로 돌릴 때 JSON.parse 함수를 사용한다.

 

 

'React' 카테고리의 다른 글

[React] chap.2  (0) 2021.12.07
[React] chap.1  (0) 2021.12.03

+ Recent posts