본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day71

by 루팽 2023. 3. 10.

yarn은 npm(node package manager)

  → import, require할 수 있음, 설치하면 package.json에 들어감

  npm i -d xxxx 혹은 yarn add xxxx같이 설치함(-d는 개발자모드)

 

yarn start하면 서버기동(포트번호 3000)

  → 디폴트페이지 index.html을 찾음

  → 이곳에 div있고 id값은 root

  → 이 div에 끼워넣기

  → single page application(SPA)이면서 화면은 변해야 한다

  → 이것을 가능하게 하는 것이 router API

 

브라우저가 DOM tree를 그려준다

  → 상속구조(부모와 자손)

  → 부모에서 자손으로만 props전달 가능(전개 연산자, 구조분해할당 필요)

  → 상위에서 자손으로 넘길 때 spread 연산자(직관적이기에), 구조분해할당

 

index.html에는 <div id=”root”></div> 만 있음

 

index.js

  App 컴포넌트를 BrowserRouter로 감싸준다

  → SPA로 처리하면서 화면이동은 필요하기 때문에 router사용

  구글 로그인에 대한 처리를 담당하는 공통 코드 authLogic 클래스를 모든 컴포넌트에서 재사용해야 하니까 index.js에서 객체생성을 하였다(const authLogic = new AuthLogic())

 

  index.js에서 인증객체 생성(authLogic.js에 설정해 둠-바닐라 스크립트)

    → AuthLogic 클래스(객체)에 필드와 함수포함되어 있음

    → 자스에서 함수는 객체이기에 주소번지를 갖는다

    → dependency array

 

생성된 객체(AuthLogic.class)는 App컴포넌트에도 전달(props)해야 한다

 

props엔 여러 가지를 담을 수 있다(주소번지도 담을 수 있으니 onLogout같은 함수도 담을 수 있다)

   이때 props를 활용하여 부모 컴포넌트의 주소번지를 자손 컴포넌트에 넘길 수 있다

  (주의: 자손 컴포넌트에서 부모 컴포넌트로는 불가함)

<BrowserRouter>
    {/* App에 props로 authLogic를 담는다(주소번지) */}
    <App authLogic={authLogic} onLogout={onLogout} />
</BrowserRouter>

 

 

useMemo / useCallback

  캐시필요(서버), 캐시에 등록하는 걸 메모리제이션(memorization)이라고함

  useMemo는 값(리턴이 와야 함), useCallback는 함수(리턴 필요 없음)를 메모리제이션함

 

App.jsx

  jsx는 tag(html)와 js 섞어 쓰기 가능 → {js문법은 중괄호 안에 씀}

 

<Routes> 안에 화면의 개수만큼 <Route>

  → <Route>의 element에는 사용자 정의 componente가 옴(URL, 이동해야 할 페이지)

 

  path를 정확히 지키려면 exact={true} → *같이 부정확하면 빼야 함

  아래 코드에서 WorkoutPage로 넘겨주는 items는 useState훅 → 리렌더링이 일어남(자동으로 동기화됨)

<>
      <Routes>
        <Route path="/workout" exact={true} element={<WorkoutPage authLogic={authLogic} workouts={items} />} />
      	<Route path=”/board/*”>
      </Routes>
</>

 

WorkoutPage → Workouts(n개의 rows-객체배열 → map사용, key 설정) → Workout(1개 row-객체배열)

{
    workouts.map((item, index) => (
    	<Workout workout(1개 row)={item} />
))}

 

 

<리액트 props 연습 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import AuthLogic from "./service/authLogic";
import firebaseApp from "./service/firebase";
import "@fortawesome/fontawesome-free/js/all.js";
import SampleApp from "./SampleApp";

// 공통코드: service -> authLogic.js -> import 외부 js 재사용 가능 -> export default 클래스명 -> module
// 브라우저에서 import하려면 반드시 babel이 필요함
const authLogic = new AuthLogic(firebaseApp); // 인스턴스화(파라미터가 올 수 있다)
// 왜 파라미터가 필요한가? - firebaseApp -> import firebaseApp from "./service/firebase"; -> export default firebaseApp
// authLogic.파이어베이스에서 제공하는 함수를 호출하겠다
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <>
    <BrowserRouter>
      {/* App컴포넌트를 렌더링할 때 속성자리에 주소번지를 넘길 수 있다 - props */}
      {/* 태그와 JS 섞어쓰기 가능 - 중괄호{} */}
      {/* <App authLogic={authLogic} /> */}
      <SampleApp />
    </BrowserRouter>
  </>
);

 

<리액트 props 연습 - SampleApp.jsx>

import React, { useState } from "react";
import SamplePage from "./components/sample/SamplePage";

const SampleApp = () => {
  const [num, setNum] = useState(0);

  const handleAdd = () => {
    console.log("SampleApp  handleAdd 호출");
    setNum(num + 1);
  };

  const handleMinus = () => {
    console.log("SampleApp  handleMinus 호출");
    setNum(num - 1);
  };

  return (
    <>
      <SamplePage num={num} handleAdd={handleAdd} handleMinus={handleMinus} />
    </>
  );
};

export default SampleApp;

 

<리액트 props 연습 - SamplePage.jsx>

import React from "react";
import SampleBottom from "./SampleBottom";
import SampleHeader from "./SampleHeader";
import SubPage from "./SubPage";

const SamplePage = (props) => {
  const handleAdd = (num) => {
    console.log("SamplePage handleAdd : " + num);
    props.handleAdd(num);
  };

  const handleMinus = (num) => {
    console.log("SamplePage handleMinus : " + num);
    props.handleMinus(num);
  };

  return (
    <>
      <SampleHeader num={props.num} />
      <div
        style={{ border: "5px solid darkred", width: "600px", height: "300px" }}
      >
        <SubPage handleAdd={handleAdd} handleMinus={handleMinus} />
      </div>
      <SampleBottom />
    </>
  );
};

export default SamplePage;

 

<리액트 props 연습 - SampleHeader.jsx>

import React from "react";

const SampleHeader = (props) => {
  return (
    <>
      <div style={{ border: "5px solid yellow", width: "600px", height:'100px' }}>
        SampleHeader페이지 영역
        <h1>{props.num}</h1>
        <br />
      </div>
    </>
  );
};

export default SampleHeader;

 

<리액트 props 연습 - SampleBottom.jsx>

import React from "react";

const SampleBottom = () => {
  return (
    <>
      <div style={{ border: "5px solid green", width: "600px", height:'100px' }}>
        SampleBottom페이지 영역
        <br />
      </div>
    </>
  );
};

export default SampleBottom;

 

<리액트 props 연습 - SubPage.jsx>

import React from "react";

const SubPage = (props) => {
  const handleAdd = () => {
    console.log("SubPage handleAdd호출 ");
    props.handleAdd(props.num);
  };

  const handleMinus = () => {
    console.log("SubPage handleMinus호출");
    props.handleMinus(props.num);
  };

  return (
    <div
      style={{ border: "5px solid darkblue", width: "300px", height: "150px" }}
    >
      SubPage
      <button onClick={handleAdd}>증가</button>
      &nbsp;
      <button onClick={handleMinus}>감소</button>
    </div>
  );
};

export default SubPage;

 

index.js에서 <App />

  → App.jsx

    이벤트처리 증가, 감소: array → 객체

  → WorkoutPage.jsx

  → Workouts.jsx

    반복문돌림 map(로우 주소번지, 인덱스, 배열객체자신) → 세 번째 파라미터는 안 씀

workouts.map((item) => (
	<Workout key={item.id} workout={item} />
))

 

    Workout.jsx는 한 개 row만 처리

    부모에서 자손으로 주소번지를 넘길 수 있다 → props로 부름

const Workout = (props) => {
    const{workout} = props
    return(
    )
}

 

<리액트 props - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import AuthLogic from "./service/authLogic";
import firebaseApp from "./service/firebase";
import "@fortawesome/fontawesome-free/js/all.js";
// import SampleApp from "./SampleApp";

// 공통코드: service -> authLogic.js -> import 외부 js 재사용 가능 -> export default 클래스명 -> module
// 브라우저에서 import하려면 반드시 babel이 필요함
const authLogic = new AuthLogic(firebaseApp); // 인스턴스화(파라미터가 올 수 있다)
// 왜 파라미터가 필요한가? - firebaseApp -> import firebaseApp from "./service/firebase"; -> export default firebaseApp
// authLogic.파이어베이스에서 제공하는 함수를 호출하겠다
// document.getElementById("root") -> index.html문서에서 DOM정보를 수집하는 것임
// const root = ReactDOM.createRoot(document.getElementById("root"));
const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render(
  <>
    <BrowserRouter>
      {/* App컴포넌트를 렌더링할 때 속성자리에 주소번지를 넘길 수 있다 - props */}
      {/* 태그와 JS 섞어쓰기 가능 - 중괄호{} */}
      <App authLogic={authLogic} />
      {/* <SampleApp /> */}
    </BrowserRouter>
  </>
);

 

<리액트 props - App.jsx>

import { Route, Routes } from "react-router-dom";
import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";
//import DeptPage from './components/page/DeptPage';
import EmpPage from "./components/page/EmpPage";
import HomePage from "./components/page/HomePage";
import FireDeptPage from "./components/page/FireDeptPage";
import LoginPage from "./components/login/LoginPage";
import BoardPage from "./components/page/BoardPage";
import WorkoutPage from "./components/page/WorkoutPage";
import HackerNewsPage from "./components/page/HackerNewsPage";
import YoutubePage from "./components/page/YoutubePage";
import { useEffect, useState } from "react";

// index.js에서 브라우저 라우터로 감싸진 App태그 속성값으로 넘어온 주소번지를 받는다
const App = ({ authLogic }) => {
  console.log("App 호출");

  // 상수값 -> 나중에 axios(NodeJS, React-모듈화), fetch(바닐라-브라우저 지원)로 처리(둘 다 비동기 처리방식)
  const [items, setItems] = useState([
    { id: 1, name: "벤치프레스", count: 0 },
    { id: 2, name: "랫풀다운", count: 0 },
    { id: 3, name: "스쿼트", count: 0 },
  ]);

  /*
    첫번째 파라미터는 콜백함수 - 객체
    두번째 파라미터는 의존성 배열 - Dependency Array
    의존성 배열이 비어있으면 최초 App 컴포넌트가 렌더링될 때 딱 한번만 실행됨
    리렌더링이 되는 대상은 return안에 있는 코드들이다
    만약 items가 변경되면 그때는 리렌더링이 일어남
  */
  useEffect(() => {
    console.log("effect 호출");
  }, []);

  const handleIncrement = (item) => {
    const index = items.indexOf(item);
    items[index].count += 1;
    setItems([...items]);
  };

  const handleDecrement = (item) => {
    const index = items.indexOf(item);
    const count = items[index].count - 1;
    items[index].count = count < 0 ? 0 : count;
    setItems([...items]);
  };

  const handleDelete = (item) => {
    console.log(`handleDelete => ${item.name}`);
    const workouts = items.filter((workout) => workout.id != item.id);
    setItems([...workouts]);
  };

  const handleAdd = (name) => {
    console.log(`handleAdd => ${name}`);
    // AddForm화면에서 사용자가 입력한 운동이름을 받아온다
    // 세번째 파라미터는 0으로 초기화
    // 스프레드 연산자를 활용하여 기존 배열에 한 개의 객체를 추가하는 코드
    const workouts = [...items, { id: Date.now(), name, count: 0 }];
    // 상태 훅에 반영 - spread 연산자를 활용하여 새로운 주소번지가 채번되도록 처리해야함
    // 상태값이 변경되었다는 사실을 리액트에서 인지할 수 있다
    setItems([...workouts]);
  };

  // 사용자 정의 컴포넌트에서 return 다음에 오는 코드가 element의 집합(컴포넌트)
  // Router를 이용하면 SPA(Single Page Application)를 누릴 수 있다
  return (
    <>
      <Routes>
        {/* 아래같이 계속 authLogic 넘기는게 불편 -> 리덕스 배우면 상태관리가능! */}
        <Route
          path="/"
          exact={true}
          element={<LoginPage authLogic={authLogic} />}
        />
        <Route
          path="/home/:userId"
          exact={true}
          element={<HomePage authLogic={authLogic} />}
        />
        <Route
          path="/board"
          exact={true}
          element={<BoardPage authLogic={authLogic} />}
        />
        <Route
          path="/workout"
          exact={true}
          element={
            <WorkoutPage
              authLogic={authLogic}
              workouts={items}
              onIncrement={handleIncrement}
              onDecrement={handleDecrement}
              onDelete={handleDelete}
              onAdd={handleAdd}
            />
          }
        />
        <Route
          path="/hackernews"
          exact={true}
          element={<HackerNewsPage authLogic={authLogic} />}
        />
        <Route
          path="/youtube"
          exact={true}
          element={<YoutubePage authLogic={authLogic} />}
        />
        <Route
          path="/dept/:id"
          exact={true}
          element={<FireDeptPage authLogic={authLogic} />}
        />
        {/* <Route path="/dept/:id" exact={true} element={<DeptPage authLogic={authLogic} />} /> */}
        <Route
          path="/emp"
          exact={true}
          element={<EmpPage authLogic={authLogic} />}
        />
      </Routes>
    </>
  );
};

export default App;

 

<리액트 props - HomePage.jsx>

import React, { useEffect } from "react";
import Header from "../include/Header";
import Bottom from "../include/Bottom";
import { useNavigate, useParams } from "react-router-dom";

// 로그아웃 처리를 위해서 props에 authLogic 주소번지를 받아온다
const HomePage = ({ authLogic }) => {
  const navigate = useNavigate();
  let { userId } = useParams();
  console.log(userId);

  /*
    onLogout이라는 변수가 함수객체를 가리키고있다 - 주소번지
    함수가 가리키는 주소번지가 다르면 리렌더링이 일어남
    
    리렌더링은 언제 발생하는가?
      1. state 상태(데이터)가 변경되었을 때
        원시형useState() / 객체-useState({}) / 배열-useState([]) / 배열객체-useState([{}])
      2. props가 변경되었을 때
        ({title, onLogout, content}) -> spread operator(ES6)
      3. 부모컴포넌트가 변경되었을 때(캡쳐링)
        map(n건) -> item별로 분할(row)
        BoardList - BoardRow - BoardDetail

      컴포넌트들이 다 함수형이다(객체이다 -> 주소번지를 가짐)
        첫글자 소문자 -> 이벤트처리함수
        첫글자 대문자 -> 컴포넌트
      컴포넌트는 렌더함수에 들어간다(렌더는 리턴에서 실제함수 그려줌)
      리턴은 컴포넌트가 불려졌을때 실행되고 렌더링됨
  */

  const onLogout = () => {
    console.log("HomePage onLogout 호출");
    authLogic.logout();
  };

  useEffect(() => {
    authLogic.onAuthChange((user) => {
      if (!user) {
        navigate("/");
      }
    });
  });

  return (
    <React.Fragment>
      <Header userId={userId} onLogout={onLogout} />
      <div>HomePage 페이지</div>
      <Bottom />
    </React.Fragment>
  );
};

export default HomePage;

 

<리액트 props - HackerNewsPage.jsx>

import axios from "axios";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
import Bottom from "../include/Bottom";
import Header from "../include/Header";

const NewsListUL = styled.ul`
  background-color: gray;
  padding-top: 10px;
  padding-left: 6px;
  padding-right: 6px;
  padding-bottom: 18px;
`

const HackerNewsPage = ({ authLogic }) => {
  const [newsList, setNewsList] = useState([]);
  const NEWSURL = "https://api.hnpwa.com/v0/news/1.json";
  const navigate = useNavigate();

  const onLogout = () => {
    console.log("HackerNewsPage onLogout 호출");
    authLogic.logout();
  };

  useEffect(() => {
    axios.get(NEWSURL).then((response) => {
      console.log(response.data);
      setNewsList(response.data);
    });
  }, []);

  useEffect(() => {
    authLogic.onAuthChange((user) => {
      if (!user) {
        navigate("/");
      }
    });
  });

  return (
    <>
      <Header onLogout={onLogout} />
      <NewsListUL>
        {newsList.map((item, index) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </NewsListUL>
      <Bottom />
    </>
  );
};

export default HackerNewsPage;

 

<리액트 props - WorkoutPage.jsx>

import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Bottom from "../include/Bottom";
import Header from "../include/Header";
import Workouts from "../workout/Workouts";

const WorkoutPage = ({ authLogic, workouts, onIncrement, onDecrement, onDelete, onAdd }) => {
  // props로 받았을 경우 구조분해할당
  // const {authLogic, workouts, onIncrement, onDecrement} = props
  const navigate = useNavigate();

  const onLogout = () => {
    console.log("WorkoutPage onLogout 호출");
    authLogic.logout();
  };

  const handleIncrement = (item) => {
    console.log("handleIncrement => " + item)
    /*
      상위 컴포넌트(App.jsx)의 함수를 호출하는 코드임
      WorkoutPage의 props에 App에서 선언된 workouts로 주소번지를 가지고 있는데
      왜 여기서는 처리를 못하고 다시 상위 컴포넌트로 미루는 것인가?
        -> setItems훅을 사용할 수 없기때문
      setItems는 파라미터로 넘기지 않는다
        -> 해당 컴포넌트의 화면 렌더링과 관련된 함수라서!
    */
    onIncrement(item);
  };

  const handleDecrement = (item) => {
    onDecrement(item);
  };
  
  const handleDelete = (item) => {
    onDelete(item);
  };

  const handleAdd = (name) => {
    // 부모에 정의된 함수 호출하기 - AddForm에서 inputRef로 설정된 값
    onAdd(name);
  };

  useEffect(() => {
    authLogic.onAuthChange((user) => {
      if (!user) {
        navigate("/");
      }
    });
  });

  return (
    <>
      <Header onLogout={onLogout} />
      <Workouts
        workouts={workouts}
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        onDelete={handleDelete}
        onAdd={handleAdd}
      />
      <Bottom />
    </>
  );
};

export default WorkoutPage;

 

<리액트 props - Workouts.jsx>

import React, { useState } from "react";
import AddForm from "./AddForm";
import Workout from "./Workout";

// props로 넘길 때 파라미터를 따로 작성하지 않아도 넘어감 - 동일한 메소드 선언 불가
const Workouts = ({ workouts, onIncrement, onDecrement, onDelete, onAdd }) => {
  const handleIncrement = (item) => {
    console.log("Workouts => " + item);
    onIncrement(item);
  };

  const handleDecrement = (item) => {
    onDecrement(item);
  };

  const handleDelete = (item) => {
    onDelete(item);
  };

  const handleAdd = (name) => {
    onAdd(name);
  };

  return (
    <>
      <div className="habits">
        <AddForm onAdd={handleAdd} />
        <ul>
          {workouts.map((item, index) => (
            <Workout
              key={index}
              workout={item}
              onIncrement={handleIncrement}
              onDecrement={handleDecrement}
              onDelete={handleDelete}
            />
          ))}
        </ul>
      </div>
    </>
  );
};

export default Workouts;

 

<리액트 props - Workout.jsx>

import React, { useState } from "react";

// const Workout = (props) => {}
// const {workout, onIncrement} = props -> 구조분해할당
// 미리 구조분해할당으로 받아옴
const Workout = ({ workout, onIncrement, onDecrement, onDelete }) => {
  console.log("Workout => " + workout);

  const handleIncrement = () => {
    // 이벤트 처리가 되어있지 않고 상위 컴포넌트의 함수를 호출하고있다
    // 상위 컴포넌트의 함수는 props를 통해서 가져온다
    // 상위 함수를 호출할 때 파라미터도 넘어간다
    onIncrement(workout);
  };

  const handleDecrement = () => {
    onDecrement(workout);
  };

  const handleDelete = () => {
    onDelete(workout);
  };

  return (
    <>
      <li className="habit">
        <span className="habit-name">{workout.name}</span>
        <span className="habit-count">{workout.count}</span>
        <button
          className="habit-button habit-increase"
          onClick={handleIncrement}
        >
          <i className="fas fa-plus-square"></i>
        </button>
        <button
          className="habit-button habit-decrease"
          onClick={handleDecrement}
        >
          <i className="fas fa-minus-square"></i>
        </button>
        <button className="habit-button habit-delete" onClick={handleDelete}>
          <i className="fas fa-trash"></i>
        </button>
      </li>
    </>
  );
};

export default Workout;

 

<리액트 props - AddForm.jsx>

import React from "react";

const AddForm = (props) => {
  // createRef는 브라우저에서 DOM요소에 접근해서 그 요소에 값이나 클릭이벤트를 등록했던 것처럼
  // 리액트는 바로 DOM 요소에 접근하지 않고 필요할때 createRef 이용해서
  // 멤버변수 정의한 다음, 그것을 원하는 해당하는 컴포넌트에 ref로 연결하면 된다
  // 전변 선언시 접미어에 Ref를 붙여서 표시
  const formRef = React.createRef();
  const inputRef = React.createRef();

  const onSubmit = (event) => {
    // 이벤트 버블링 이슈 해결하기위해 반드시 추가할 것 - onSubmit이슈
    event.preventDefault();
    const name = inputRef.current.value;
    name && props.onAdd(name);
    formRef.current.reset();
  };

  return (
    <form ref={formRef} className="add-for" onSubmit={onSubmit}>
      <input
        ref={inputRef} //이렇게 하면 위에서 선언한 전역변수와 연결됨
        type="text"
        className="add-input"
        placeholder="Workout"
      />
      <button className="add-button">Add</button>
    </form>
  );
};

export default AddForm;

댓글