프로그래밍/React

[React] 초보자를 위한 리액트 강좌 3

라다디 2022. 1. 23. 20:26

 

더미 데이터 구현, map() 반복문

db폴더 생성 - data.json 파일 생성

데이터 - https://github.com/coding-angma/voca/blob/lesson/9/src/db/data.json

 

@ DayList 컴포넌트 생성

day 개수만큼 <li>를 만들어줘야 함 -> map 사용

import dummy from "../db/data.json";

export default function DayList() {
  console.log(dummy);
  return (
    <ul className="list_day">
      {dummy.days.map((day) => (
        <li>Day {day.day}</li>
      ))}
    </ul>
  );
}

이 상태에서 console을 보면 경고문이 뜸

Warning: Each child in a list should have a unique "key" prop.

반복되는 요소에 고유한 값을 넣어주는 용도로 key 사용

<li key={day.id}>Day {day.day}</li>

 

* 리액트에서 key가 필요한 이유

https://www.youtube.com/watch?v=ghxHAy3LH28 

리액트에서 key는 요소의 변화를 알아차리기 위해 필요

 

@ Day 컴포넌트 생성

import dummy from "../db/data.json";

export default function Day() {
  return (
    <>
      <table>
        <tbody>
          {dummy.words.map((word) => (
            <tr>
              <td>{word.eng}</td>
              <td>{word.kor}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";

function App() {
  return (
    <div className="App">
      <Header />
      <DayList />
      <Day />
    </div>
  );
}

export default App;

문제점1: 모든 단어가 다 나오고 있음

 

@ filter를 사용해서 Day를 고정 (ex. Day1)

import dummy from "../db/data.json";

export default function Day() {
  // dummy.words
  const day = 1;
  const wordList = dummy.words.filter((word) => word.day === day);
  console.log(wordList);

  return (
    <>
      <table>
        <tbody>
          {wordList.map((word) => (
            <tr key={word.id}>
              <td>{word.eng}</td>
              <td>{word.kor}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

문제점2: DayList 밑에 단어가 뜨는 게 아니라 Day를 클릭했을 때 다른 페이지에서 단어가 떠야 함

-> 다른 페이지를 만들어보자 

 

 

라우터 구현

react-router-dom 버전 v5로 구현하는 코드 (최신 버전 v6)
v6 삭제 : npm uninstall react-router-dom
v5 설치 : npm install react-router-dom@5.2.0

리액트 라우터 돔 설치

npm install react-router-dom

 

1. import 후 BrouserRouter로 App 전체를 감쌈

2. Header는 모든 페이지에 나와야 하니까 Header 다음 부분을 Switch로 감쌈

-> Switch 내부는 url에 따라 각기 다른 페이지를 보여줌

-> Switch 외부는 모든 페이지에 공통으로 노출

4. path는 주소

-> "/" = 첫 페이지를 의미

-> <Route path="/">와 <Route path="/day">가 있다면 두 번째 코드에 "/"가 포함되어 있어 첫 페이지로 이동되므로 <Route exact path="/"> 이런식으로 작성

import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";
import { BrowserRouter, Route, Switch } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Header />
        <Switch>
          <Route exact path="/">
            <DayList />
          </Route>
          <Route path="/day">
            <Day />
          </Route>
        </Switch>
      </div>
    </BrowserRouter>
  );
}

export default App;

 

HTML은 a태그를 사용하지만 리액트 라우터는 Link를 사용

 

@ Day 클릭시 단어들 리스트로 넘어가기

// DayList.js
import { Link } from "react-router-dom";
import dummy from "../db/data.json";

export default function DayList() {
  console.log(dummy);
  return (
    <ul className="list_day">
      {dummy.days.map((day) => (
        <li key={day.id}>
          <Link to={`/day/${day.day}`}>Day {day.day}</Link>
        </li>
      ))}
    </ul>
  );
}

 @ 페이지 제목 클릭시 첫 번째 페이지로 넘어가기

// Header.js
import { Link } from "react-router-dom";

export default function Header() {
  return (
    <div className="header">
      <h1>
        <Link to="/">토익 영단어(고급)</Link>
      </h1>
      <div className="menu">
        <a href="#x" className="link">
          단어 추가
        </a>
        <a href="#x" className="link">
          Day 추가
        </a>
      </div>
    </div>
  );
}

 

리액트에서 다이나믹한 URL을 처리할 때는 ":"으로 처리

// App.js
...
<Route path="/day/:day">
...

만일 http://localhost:3000/day/1 이런식의 URL로 들어왔다면 day라는 변수로 1의 값을 얻을 수 있음

"/day/:id"였다면 id라는 변수로 1의 값을 얻을 수 있는 것

 

URL에 포함된 값을 얻을 때는 리액트 라우터 돔에서 제공하는 useParams 훅을 사용

주소가 http://localhost:3000/day/3과 같을 때 

const a = useParams();
console.log(a);

콘솔을 보면 {day: '3'}이런식으로 찍힘

<Route path="/day/:id">였다면 {id: '3'}으로 나오게 됨

 

@ 특정 날짜로 이동 

// Days.js
...
export default function Day() {
  // (1)
  const a = useParams();
  const day = a.day;
  
  // (2)
  const day = useParams().day;
  
  // (3)
  const { day } = useParams();
  
  const wordList = dummy.words.filter((word) => word.day === day);
...

단어들은 안나오는 상태 -> 문자열과 숫자를 비교하고 있기 때문

word.day는 숫자인 반면 day는 문자열

-> Number(day)로 바꿔주자

 

@ 없는 주소 처리

// EmptyPage.js
import { Link } from "react-router-dom";

export default function Day() {
  return (
    <>
      <h2>잘못된 접근입니다.</h2>
      <Link to="/">돌아가기</Link>
    </>
  );
}
// App.js
import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";
import EmptyPage from "./component/EmptyPage";
import { BrowserRouter, Route, Switch } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Header />
        <Switch>
          <Route exact path="/">
            <DayList />
          </Route>
          <Route path="/day/:day">
            <Day />
          </Route>
          <Route>
            <EmptyPage />
          </Route>
        </Switch>
      </div>
    </BrowserRouter>
  );
}

export default App;

Route에 path를 적어주지 않고 제일 밑에 적어줌

앞에 있는 조건이 모두 만족하지 않으면 EmptyPage가 보여지게 됨

만일 제일 위에 적어두면 모든 페이지가 EmptyPage로 가게 됨

 

 

json-server, REST API

@ 체크박스, 뜻 보기, 삭제 버튼 추가

// Days.js
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";

export default function Day() {
  const { day } = useParams();
  const wordList = dummy.words.filter((word) => word.day === Number(day));
  console.log(wordList);

  return (
    <>
      <h2>Day {day}</h2>
      <table>
        <tbody>
          {wordList.map((word) => (
            <tr key={word.id}>
              <td>
                <input type="checkbox" />
              </td>
              <td>{word.eng}</td>
              <td>{word.kor}</td>
              <td>
                <button>뜻 보기</button>
                <button className="btn_del">삭제</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

 

@ 뜻 보기 작업 - 별도의 컴포넌트에서 수행

: 각 컴포넌트별로 state를 가짐 -> 뜻 보기를 눌렀을 때 뜻이 나타났다 없어졌다 하는 건 단어 하나에만 해당하는 거이기 때문에 따로 컴포넌트를 제작해주는 것이 좋음

// Word.js
export default function Word({ word }) {
  return (
    <tr>
      <td>
        <input type="checkbox" />
      </td>
      <td>{word.eng}</td>
      <td>{word.kor}</td>
      <td>
        <button>뜻 보기</button>
        <button className="btn_del">삭제</button>
      </td>
    </tr>
  );
}
// Day.js
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";
import Word from "./Word";

export default function Day() {
  const { day } = useParams();
  const wordList = dummy.words.filter((word) => word.day === Number(day));
  console.log(wordList);

  return (
    <>
      <h2>Day {day}</h2>
      <table>
        <tbody>
          {wordList.map((word) => (
            <Word word={word} key={word.ud} />
          ))}
        </tbody>
      </table>
    </>
  );
}

 

@ 뜻 보기/숨기기 기능 구현

import { useState } from "react";

export default function Word({ word }) {
  const [isShow, setIsShow] = useState(false);

  function toggleShow() {
    setIsShow(!isShow);
  }

  return (
    <tr>
      <td>
        <input type="checkbox" />
      </td>
      <td>{word.eng}</td>
      <td>{isShow && word.kor}</td>
      <td>
        <button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
        <button className="btn_del">삭제</button>
      </td>
    </tr>
  );
}

 

@ 외운 단어 체크, 못 외운 단어 미체크

import { useState } from "react";

export default function Word({ word }) {
  const [isShow, setIsShow] = useState(false);

  function toggleShow() {
    setIsShow(!isShow);
  }

  return (
    <tr className={word.isDone ? "off" : ""}>
      <td>
        <input type="checkbox" checked={word.isDone} />
      </td>
      <td>{word.eng}</td>
      <td>{isShow && word.kor}</td>
      <td>
        <button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
        <button className="btn_del">삭제</button>
      </td>
    </tr>
  );
}

이렇게 구현했을 시 콘솔창을 보면 아래와 같은 경고 문구가 보임

Warning: You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly` .

어떤 액션을 취하더라도 checked가 값이 고정되어 있으므로 읽기 전용이랑 다를 바가 없다는 뜻

-> onChange를 추가해서 반응형으로 만들기

import { useState } from "react";

export default function Word({ word }) {
  const [isShow, setIsShow] = useState(false);
  const [isDone, setIsDone] = useState(word.isDone);

  function toggleShow() {
    setIsShow(!isShow);
  }

  function toggleDone() {
    setIsShow(!isDone);
  }

  return (
    <tr className={isDone ? "off" : ""}>
      <td>
        <input type="checkbox" checked={word.isDone} onChange={toggleDone}/>
      </td>
      <td>{word.eng}</td>
      <td>{isShow && word.kor}</td>
      <td>
        <button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
        <button className="btn_del">삭제</button>
      </td>
    </tr>
  );
}

 

여기까지의 결과

 

@ 사용자 액션에 따라 데이터 CRUD

-> DB구축, API를 만들어야 함 (json-server를 이용해서 REST API를 만들기)

json-server는 빠르고 쉽게 REST API를 구축해줌

 

npm install -g json-server

 

json-server --watch ./src/db/data.json --port 3001

이제 localhost:3001/days,localhost:3001/words로 가보면 데이터들이 보임

 

REST API란?

URI주소와 메소드로 CRUD요청을 하는 것

Create : Post

Read : GET

Update : PUT

Delete : DELETE

 

 

useEffect, fetch()로 API 호출

useEffect는 어떤 상태값이 바뀌었을 때 동작하는 함수를 작성할 수 있는 훅

첫 번째 매개변수로 함수를 넣음 :  이 함수가 호출 된 타이밍은 렌더링 결과가 실제 돔에 반영된 직후, 그리고 컴포넌트가 사라지기 직전에도 마지막으로 호출

 

@ 연습

// DayList.js
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";

export default function DayList() {
  const [days, setDays] = useState([]);
  const [count, setCount] = useState(0);

  function onClick() {
    setCount(count + 1);
  }

  function onClick2() {
    setDays([
      ...days,
      {
        id: Math.random(),
        day: 1,
      },
    ]);
  }

  useEffect(() => {
    console.log("Count change");
  });

  return (
    <>
      <ul className="list_day">
        {days.map((day) => (
          <li key={day.id}>
            <Link to={`/day/${day.day}`}>Day {day.day}</Link>
          </li>
        ))}
      </ul>
      <button onClick={onClick}>{count}</button>
      <button onClick={onClick2}>Day Change</button>
    </>
  );
}

위와 같이 코드를 작성했을 때 Day Change버튼을 누르면 count는 변경되지 않았음에도 불구하고 console에 Count Change가 찍히는 것을 볼 수 있음

= useEffect의 함수가 불필요하게 호출되고 있음

-> useEffect의 두 번째 매개변수로 배열 넣기

useEffect(() => {
  console.log("Count change");
}, [count]);

이런식으로 작성을 해주면 count가 변경이 되었을 때만 함수가 실행 됨

이런 것을 의존성 배열이라고 함

의존성 배열의 값이 변경되는 경우에만 함수가 실행

 

useEffect를 여기서 사용하는 목적은 API호출

렌더링이 완료된 후 최초에 한 번만 API를 호출해주면 됨

이런 경우에는 의존성 배열을 빈 배열로 입력함

count를 누르고 Day Change를 계속 눌러도 Count change는 한 번만 찍힘

이렇게 상태값과 무관하게 렌더링 직후 딱 한 번만 실행되는 작업은 빈 배열을 전달하면 됨

 

@ API 호출

API 비동기 통신을 위해서 fetch를 이용

useEffect(() => {
  fetch("http://localhost:3001/days") // API 경로, 프로미스가 반환
    .then((res) => {  // response는 http응답이고 실제 json은 아님
      return res.json(); // 그래서 json메소드를 사용해서 json으로 변환
    })
    .then((data) => {
      setDays(data);
    });
}, []);

네트워크 탭을 확인해보면 days호출이 잘 된 것을 볼 수 있음

 

@ dummy데이터 사용했던 부분 수정

import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Word from "./Word";

export default function Day() {
  const { day } = useParams();
  const [words, setWords] = useState([]);

  useEffect(() => {
    fetch(`http://localhost:3001/words?day=${day}`)
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setWords(data);
      });
  }, []); // (1)

  return (
    <>
      <h2>Day {day}</h2>
      <table>
        <tbody>
          {words.map((word) => (
            <Word word={word} key={word.ud} />
          ))}
        </tbody>
      </table>
    </>
  );
}

그런데 이렇게 코드를 작성하면 (1)부분에서 day가 의존성 배열에 없다는 경고가 뜨게 됨

: useEffect내부에서 day와 같이 특정값을 사용하면 의존성 배열에 입력하라는 뜻

의존성 배열이 비어있으면 day가 변경되어도 새로운 정보를 가져올 수가 없음

의존성 배열에 day를 넣어주게 되면 이 값이 최신값이라고 보장받을 수 있음

 

아래와 같이 의존성 배열에 day를 넣어주자

useEffect(() => {
  fetch(`http://localhost:3001/words?day=${day}`)
    .then((res) => {  
      return res.json();
    })
    .then((data) => {
      setWords(data);
    });
}, [day]);

 

그런데 DayList와 Day가 상당히 비슷함

주소를 제외하고 state를 만들고 fetch로 리스트를 가져와서 set을 해준다는 동작들은 동일함

동일한 로직은 사용자가 직접 훅을 만들어서 사용할 수 있음

= Custom Hook

 

 

Custom Hooks

hooks폴더를 만들고 useFetch.js파일 생성

import { useEffect, useState } from "react";

export default function useFetch(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch(url)
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setData(data);
      });
  }, [url]);

  return data;
}

API주소를 url로 넘겨받아서 fetch를 해주고 의존성 배열도 바뀔 수 있으므로 url을 입력해줌

필요로 하는 것은 data이므로 return 

 

@ 적용

import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";

export default function DayList() {
  const days = useFetch("http://localhost:3001/days");

  return (
    <ul className="list_day">
      {days.map((day) => (
        <li key={day.id}>
          <Link to={`/day/${day.day}`}>Day {day.day}</Link>
        </li>
      ))}
    </ul>
  );
}
import { useParams } from "react-router-dom";
import Word from "./Word";

export default function Day() {
  const { day } = useParams();
  const words = useFetch(`http://localhost:3001/words?day=${day}`);

  return (
    <>
      <h2>Day {day}</h2>
      <table>
        <tbody>
          {words.map((word) => (
            <Word word={word} key={word.ud} />
          ))}
        </tbody>
      </table>
    </>
  );
}

📌 아래 강의의 내용을 정리한 글입니다.