React로 데이터 다루기

LEE GYUHO·2023년 10월 27일
0

📌배열 렌더링

  • 📘VSCode 기능

    • 멀티 커서 선택하기: Ctrl + Shift + L

    • 직접 멀티 커서 만들기: Alt + 클릭

    • 찾아 바꾸기: F2

    • 해당하는 파일로 이동: Ctrl + 클릭

    • 줄 이동: Alt +↑↓

    • 줄 복사: Alt + Shift +↑↓

    단축키들은 VSCode의 메뉴에서 Help > Keyboard Shortcuts Reference 를 통해서 확인할 수도 있다.

  • 📘map으로 배열 렌더링하기

//App.js
import ReviewList from './ReviewList';
import items from '../mock.json';

function App() {
  return (
    <div>
      <ReviewList items={items} />
    </div>
  );
}

export default App;

-----------------------------------------------------------

//ReviewList.js
import './ReviewList.css';

function formatDate(value) {
  const date = new Date(value);
  return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
}

function ReviewListItem({ item }) {
  return (
    <div className="ReviewListItem">
      <img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
      <div>
        <h1>{item.title}</h1>
        <p>{item.rating}</p>
        <p>{formatDate(item.createdAt)}</p>
        <p>{item.content}</p>
      </div>
    </div>
  );
}

function ReviewList({ items }) {
  return (
    <ul>
      {items.map((item) => {
        return (
          <li>
            <ReviewListItem item={item} />
          </li>
        );
      })}
    </ul>
  );
}

export default ReviewList;
  • 📘sort로 정렬 바꾸기
import { useState } from 'react';
import ReviewList from './ReviewList';
import items from '../mock.json';

function App() {
  const [order, setOrder] = useState('createdAt');
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} />
    </div>
  );
}

export default App;
  • 📘filter로 아이템 삭제하기
//App.js
import { useState } from 'react';
import ReviewList from './ReviewList';
import mockItems from '../mock.json';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [items, setItems] = useState(mockItems);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
    </div>
  );
}

export default App;

-----------------------------------------------------------
  
//ReviewList.js
import './ReviewList.css';

function formatDate(value) {
  const date = new Date(value);
  return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
}

function ReviewListItem({ item, onDelete }) {
  const handleDeleteClick = () => {
    onDelete(item.id);
  };

  return (
    <div className="ReviewListItem">
      <img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
      <div>
        <h1>{item.title}</h1>
        <p>{item.rating}</p>
        <p>{formatDate(item.createdAt)}</p>
        <p>{item.content}</p>
        <button onClick={handleDeleteClick}>삭제</button>
      </div>
    </div>
  );
}

function ReviewList({ items, onDelete }) {
  return (
    <ul>
      {items.map((item) => {
        return (
          <li>
            <ReviewListItem item={item} onDelete={onDelete} />
          </li>
        );
      })}
    </ul>
  );
}

export default ReviewList;
  • 📘배열을 렌더링 할 땐 key를 기억

배열을 랜더링 할 때는 반드시 키값을 설정해줘야 한다.(요소들의 순서가 바뀌면 엉뚱한 위치에 랜더링될 수 있기 때문이다)
id값 같이 각 데이터를 구분할 수 있는 고유한 값으로 지정해야한다.

function ReviewList({ items, onDelete }) {
  return (
    <ul>
      {items.map((item) => {
        return (
          <li key={item.id}>
            <ReviewListItem item={item} onDelete={onDelete} />
          </li>
        );
      })}
    </ul>
  );
}

export default ReviewList;

📌데이터 가져오기

여러분이 실습하실 때는 /api 대신에 /0000 ~ /9999 사이의 숫자 네 자리로 사용

예를들어서 저는 주소를 쉽게 외우기 위해서 제 전화번호 뒷자리인 1636을 사용하려고 하는데요.

이런 경우에는 다음과 같은 주소를 사용하면 됩니다.

https://learn.codeit.kr/1636/film-reviews/
https://learn.codeit.kr/1636/foods/

  • 📘리액트에서 fetch 사용하기
//api.js
export async function getReviews() {
  const response = await fetch('https://learn.codeit.kr/api/film-reviews');
  const body = await response.json();
  return body;
}



//App.js
import { useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoadClick = async () => {
    const { reviews } = await getReviews();
    setItems(reviews);
  };

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      <button onClick={handleLoadClick}>불러오기</button>
    </div>
  );
}

export default App;
  • 📘 useEffect로 초기 데이터 가져오기
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async () => {
    const { reviews } = await getReviews();
    setItems(reviews);
  };

  useEffect(() => {
    handleLoad();
  }, []);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
    </div>
  );
}

export default App;

처음 app 컴포넌트를 랜더링 할 때는 리액트는 app 컴포넌트 함수를 실행한다.

코드를 하나씩 실행하다가 handleLoad() 함수를 실행한다.

(handleLoad() 함수는 비동기 함수니까 현재 실행중인 컴포넌트 함수와는 별도로 시작되었다고 생각)

그리고 다음 코드로 넘어가서 리액트 앨리먼트를 리턴하고 리액트는 화면을 그려줄 것이다.

다음으로 handleLoad() 함수를 보면 비동기로 리퀘스트를 보냈다가 response가 도착하면 reviews라는 변수를 지정하고 setItems를 통해서 state를 변경해준다.

그럼 리액트는 app 컴포넌트를 다시 랜더링하기 때문에 app 컴포넌트 함수를 실행하게 된다.

이때 또 다시 handleLoad() 함수를 실행해서 state가 또 바뀌고 다시 랜더링 되기 때문에 무한루프가 발생한다.


리액트에서 이런 경우에 사용할 수 있는 함수가 있는데 그것이 useEffect이다.

useEffect함수에 실행 할 콜백 함수와 빈 배열을 넘겨주면 리액트는 콜백함수를 맨 처음 랜더링 할 때만 실행하기 때문에 무한루프가 생기는 것을 막을 수 있다.

  • 📘서버에서 정렬한 데이터 받아오기
    useEffect 함수는 맨처음 랜더링이 끝나면 콜백함수를 실행하고 그 다음부터는 디펜던시 리스트를 비교해서 기억했던 값이랑 다른 경우에만 콜백을 실행해준다.
    (디펜던시 리스트는 여기서는 빈 배열을 말한다.)

    useEffect는 아규먼트로 콜백과 디펜던시 리스트를 받는다.
    리액트는 콜백함수와 디펜던시 리스트를 기억해뒀다가 처음 랜더링하고나서 콜백을 실행하고 그 이후 랜더링부터는 디펜던시 리스트에 있는 값들이 바뀌었을때만 콜백을 실행한다. 이런식으로 useEffect를 사용하면 처음 랜더링 했을 때 코드를 실행하거나 특정값이 바뀔때마다 코드를 실행할 수 있다.

//api.js
export async function getReviews(order = 'createdAt') {
  const query = `order=${order}`;
  const response = await fetch(
    `https://learn.codeit.kr/api/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}



//App.js
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (orderQuery) => {
    const { reviews } = await getReviews(orderQuery);
    setItems(reviews);
  };

  useEffect(() => {
    handleLoad(order);
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
    </div>
  );
}

export default App;
  • 📘페이지네이션(pagination)
    책의 페이지처럼 데이터를 나눠서 제공하는 것
    (많은 양의 데이터를 제공할 때 사용하는 방법)

    • 오프셋 기반 페이지네이션 (받아온 개수 기준)
      오프셋(Offset) = 상쇄하다(지금까지 받아온 데이터의 개수)

      //Request(GET https://example.com/posts?offset=20&limit=10)
      //= 지금까지 20개 받았으니까 10개 더 보내줘
              
      //Response
      {
      	"paging":{
              "count": 30,
              "hasNext": false
         },
         "posts": [...]
      }
      //서버는 Response로 데이터를 보내준다.
      //paging이라는 프로퍼티도 있는데 보통 이런식으로 페이지네이션에 대한 정보도 함께 보내준다.
      //hasNext는 다음 데이터가 있는지 없는지 알려주는 것

      오프셋 기반의 문제점: 30개의 데이터가 있을 때 20개 10개 이렇게 두번에 나눠서 데이터를 받는다고 했을 때 맨 앞에 데이터 1개가 추가되면 먼저 20개를 받을 때 추가된 데이터 + 1~19까지 받고 20~29까지 받아서 1개를 못받게된다. 이런식의 문제가 존재한다.

    • 커서 기반 페이지네이션 (데이터를 가리키는 커서 기준)
      커서(Cursor)
      = 데이터를 가리키는 값(지금까지 받은 데이터를 표시한 책갈피)

      // Request(GET https://example.com/posts?limit=10)
      // = 데이터 10개 보내줘
      
      //Response
      {
      	"paging":{
              "count": 30,
              "nextCursor": "WerZxc"
         },
         "posts": [...]
      }
      //데이터와 페이지네이션 정보를 보내준다.
      //페이지네이션 정보에 다음 커서 값까지 보내준다.
      //다음 페이지를 불러올 때는 받아온 nextCursor값으로 리퀘스트를 보내는데
      //이 커서를 기준으로 서버에 데이터를 요청한다.
       // Request(GET https://example.com/posts?cursor=WerZxc&limit=10)
      // = 커서 데이터 이후로 10개 보내줘
      //커서를 사용하면 만약 데이터가 바뀌더라도 커서가 가리키는 데이터는 변하지 않는다. 
      //그래서 offset과 다르게 데이터의 중복이나 빠짐없이 가져올 수 있다는 장점이 있다.
      //
  • 📘데이터 더 불러오기

//api.js
export async function getReviews({
  order = 'createdAt',
  offset = 0,
  limit = 6,
}) {
  const query = `order=${order}&offset=${offset}&limit=${limit}`;
  const response = await fetch(
    `https://learn.codeit.kr/api/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}


//App.js
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    const { paging, reviews } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems([...items, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      <button disabled={!hasNext} onClick={handleLoadMore}>
        더 보기
      </button>
    </div>
  );
}

export default App;
  • 📘데이터가 있을 때만 버튼 보여주기
    조건부 랜더링
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    const { paging, reviews } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems([...items, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && <button onClick={handleLoadMore}>더 보기</button>}
    </div>
  );
}

export default App;
  • 📘조건부 랜더링 꿀팁

    • 논리 연산자 활용하기

      • AND 연산자
      import { useState } from 'react';
      
      function App() {
        const [show, setShow] = useState(false);
      
        const handleClick = () => setShow(!show);
      
        return (
          <div>
            <button onClick={handleClick}>토글</button>
            {show && <p>보인다 👀</p>}
          </div>
        );
      }
      
      export default App;
      //show 값이 true 이면 렌더링 하고, false 이면 렌더링 하지 않습니다.
      • OR 연산자
      import { useState } from 'react';
      
      function App() {
        const [hide, setHide] = useState(true);
      
        const handleClick = () => setHide(!hide);
      
        return (
          <div>
            <button onClick={handleClick}>토글</button>
            {hide || <p>보인다 👀</p>}
          </div>
        );
      }
      
      export default App;
      //hide 값이 true 이면 렌더링 하지 않고, false 이면 렌더링 합니다.
      • 삼항 연산자 활용하기
      import { useState } from 'react';
      
      function App() {
        const [toggle, setToggle] = useState(false);
      
        const handleClick = () => setToggle(!toggle);
      
        return (
          <div>
            <button onClick={handleClick}>토글</button>
            {toggle ? <p>✅</p> : <p></p>}
          </div>
        );
      }
      
      export default App;
      //삼항 연산자를 사용하면 참, 거짓일 경우에 다르게 렌더링해줄 수 있습니다.
      //toggle 의 값이 참일 경우엔 '✅'을, 거짓일 경우에는 '❎'를 렌더링합니다.
      • 렌더링되지 않는 값들
      function App() {
        const nullValue = null;
        const undefinedValue = undefined;
        const trueValue = true;
        const falseValue = false;
        const emptyString = '';
        const emptyArray = [];
      
        return (
          <div>
            <p>{nullValue}</p>
            <p>{undefinedValue}</p>
            <p>{trueValue}</p>
            <p>{falseValue}</p>
            <p>{emptyString}</p>
            <p>{emptyArray}</p>
          </div>
        );
      }
      
      export default App;
      //위 컴포넌트에서 중괄호 안에 있는 값들은 모두 아무것도 렌더링하지 않습니다.
      
      function App() {
        const zero = 0;
        const one = 1;
      
        return (
          <div>
            <p>{zero}</p>
            <p>{one}</p>
          </div>
        );
      }
      
      export default App;
      //반면에 이 값들은 각각 숫자 0과 1을 렌더링 합니다.
      • 조건부 렌더링을 사용할 때 주의할 점
        (만약 아래와 같은 코드를 사용하면 어떤 문제가 있을까요?)
      import { useState } from 'react';
      
      function App() {
        const [num, setNum] = useState(0);
      
        const handleClick = () => setNum(num + 1);
      
        return (
          <div>
            <button onClick={handleClick}>더하기</button>
            {num && <p>num이 0 보다 크다!</p>}
          </div>
        );
      }
      
      export default App;
      //num 값이 0일 때는 false 로 계산되니까 뒤의 값을 계산하지 않기 때문에
      //아무것도 렌더링 하지 않는 코드 같습니다.
      //하지만 앞에서 살펴봤듯이 숫자 0은 0으로 렌더링 되는데요.
      //그래서 처음 실행했을 때 숫자 0이 렌더링 되고
      //'더하기' 버튼을 눌러서 num 값이 증가하면 num이 0 보다 크다! 가 렌더링 됩니다.
      //그래서 이런 경우엔 아래처럼 보다 명확한 논리식을 써주는 게 안전합니다.
      //true 나 false 값은 리액트에서 렌더링 하지 않기 때문이죠!
      
      import { useState } from 'react';
      
      function App() {
        const [num, setNum] = useState(0);
      
        const handleClick = () => setNum(num + 1);
      
        return (
          <div>
            <button onClick={handleClick}>더하기</button>
            {(num > 0) && <p>num이 0 보다 크다!</p>}
          </div>
        );
      }
      
      export default App;
  • 📘비동기로 state를 변경할 때 주의할 점
    비동기 상황에서 state를 변경할 때 이전 state값을 사용하려면 setter함수에서 콜백을 사용해서 이전 state를 사용하면 된다.

import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    const { paging, reviews } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && <button onClick={handleLoadMore}>더 보기</button>}
    </div>
  );
}

export default App;
  • 📘useState 뽀개기

    • 초깃값 지정하기
    const [state, setState] = useState(initialState);
    • 콜백으로 초깃값 지정하기
    const [state, setState] = useState(() => {
      // 초기값을 계산
      return initialState;
    });
    //초깃값을 계산해서 넣는 경우 이렇게 콜백을 사용하면 좋습니다.
    • Setter 함수 사용하기

      • 기본
      const [state, setState] = useState(0);
      
      const handleAddClick = () => {
        setState(state + 1);
      }
      //배열이나 객체 같은 참조형은 반드시 새로운 값을 만들어서 전달해야 한다
      
      //참조형 State 사용의 잘못된 예
      const [state, setState] = useState({ count: 0 });
      
      const handleAddClick = () => {
        state.count += 1; // 참조형 변수의 프로퍼티를 수정
        setState(state); // 참조형이기 때문에 변수의 값(레퍼런스)는 변하지 않음
      }
      	
      
      //참조형 State 사용의 올바른 예
      const [state, setState] = useState({ count: 0 });
      
      const handleAddClick = () => {
        setState({ ...state, count: state.count + 1 }); // 새로운 객체 생성
      }
      • 콜백으로 State 변경
      setState((prevState) => {
        // 다음 State 값을 계산
        return nextState;
      });
      
      //만약 이전 State 값을 참조하면서 State를 변경하는 경우,
      //비동기 함수에서 State를 변경하게 되면 최신 값이 아닌 State 값을 참조하는 문제가 있었습니다.
      //이럴 때는 콜백을 사용해서 처리할 수 있었는데요. 파라미터로 올바른 State 값을 가져와서 사용할 수 있습니다.
      //이전 State 값으로 새로운 State를 만드는 경우엔 항상 콜백 형태를 사용하는 습관을 들이면 좋겠죠?
      
      //콜백으로 State를 변경하는 예시
      const [count, setCount] = useState(0);
      
      const handleAddClick = async () => {
        await addCount();
        setCount((prevCount) => prevCount + 1);
      }
  • 📘네트워크 로딩 처리하기

import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    let result;
    try {
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      console.error(error);
      return;
    } finally {
      setIsLoading(false);
    }

    const { paging, reviews } = result;
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && (
        <button disabled={isLoading} onClick={handleLoadMore}>
          더 보기
        </button>
      )}
    </div>
  );
}

export default App;
  • 📘네트워크 에러 처리하기
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [loadingError, setLoadingError] = useState(null);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    let result;
    try {
      setLoadingError(null);
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      setLoadingError(error);
      return;
    } finally {
      setIsLoading(false);
    }

    const { paging, reviews } = result;
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && (
        <button disabled={isLoading} onClick={handleLoadMore}>
          더 보기
        </button>
      )}
      {loadingError?.message && <span>{loadingError.message}</span>}
    </div>
  );
}

export default App;

옵셔널 체이닝: ?.은 ?.'앞’의 평가 대상이 undefined나 null이면 평가를 멈추고 undefined를 반환합니다.
위의 코드에서는 loadingError가 있을때만 message 프로퍼티를 참조하여 span태그로 랜더링해준다.

📌입력 폼 다루기

  • 📘리액트에서 입력 폼 만들기
//ReviewForm.js
import { useState } from 'react';
import './ReviewForm.css';

function ReviewForm() {
  const [title, setTitle] = useState('');
  const [rating, setRating] = useState(0);
  const [content, setContent] = useState('');

  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  const handleRatingChange = (e) => {
    const nextRating = Number(e.target.value) || 0;
    setRating(nextRating);
  };

  const handleContentChange = (e) => {
    setContent(e.target.value);
  };

  return (
    <form className="ReviewForm">
      <input value={title} onChange={handleTitleChange} />
      <input type="number" value={rating} onChange={handleRatingChange} />
      <textarea value={content} onChange={handleContentChange} />
    </form>
  );
}

export default ReviewForm;
  • 📘onSubmit
import { useState } from 'react';
import './ReviewForm.css';

function ReviewForm() {
  const [title, setTitle] = useState('');
  const [rating, setRating] = useState(0);
  const [content, setContent] = useState('');

  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  const handleRatingChange = (e) => {
    const nextRating = Number(e.target.value);
    setRating(nextRating);
  };

  const handleContentChange = (e) => {
    setContent(e.target.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      title,
      rating,
      content,
    });
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <input value={title} onChange={handleTitleChange} />
      <input type="number" value={rating} onChange={handleRatingChange} />
      <textarea value={content} onChange={handleContentChange} />
      <button type="submit">확인</button>
    </form>
  );
}

export default ReviewForm;
  • 📘하나의 state로 폼 구현하기
import { useState } from 'react';
import './ReviewForm.css';

function ReviewForm() {
  const [values, setValues] = useState({
    title: '',
    rating: 0,
    content: '',
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(values);
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <input name="title" value={values.title} onChange={handleChange} />
      <input type="number" name="rating" value={values.rating} onChange={handleChange} />
      <textarea name="content" value={values.content} onChange={handleChange} />
      <button type="submit">확인</button>
    </form>
  );
}

export default ReviewForm;
  • 📘제어 컴포넌트와 비제어 컴포넌트

    • 제어 컴포넌트(Controlled Component)

      • 인풋의 value 값을 리액트에서 지정
      • 리액트에서 사용하는 값과 실제 인풋 값이 항상 일치
      • 주로 권장되는 방법
      function TripSearchForm() {
        const [values, setValues] = useState({
          location: 'Seoul',
          checkIn: '2022-01-01',
          checkOut: '2022-01-02',
        })
      
        const handleChange = (e) => {
          const { name, value } = e.target;
          setValues((prevValues) => ({
            ...prevValues,
            [name]: value,
          }));
        }
      
        return (
          <form>
            <h1>검색 시작하기</h1>
            <label htmlFor="location">위치</label>
            <input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={handleChange} />
            <label htmlFor="checkIn">체크인</label>
            <input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={handleChange} />
            <label htmlFor="checkOut">체크아웃</label>
            <input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={handleChange} />
            <button type="submit">검색</button>
          </form>
        )
      }

      			function TripSearchForm({ values, onChange }) {
                    return (
                      <form>
                        <h1>검색 시작하기</h1>
                        <label htmlFor="location">위치</label>
                        <input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={onChange} />
                        <label htmlFor="checkIn">체크인</label>
                        <input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={onChange} />
                        <label htmlFor="checkOut">체크아웃</label>
                        <input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={onChange} />
                        <button type="submit">검색</button>
                      </form>
                    )
                  }
    • 비제어 컴포넌트(Uncontrolled Component)
      • 인풋의 value 값을 리액트에서 지정하지 않음
      • 경우에 따라서 필요한 방법
  • 📘입력 폼 뽀개기

    • HTML과 다른 점
      • onChange
        리액트에선 순수 HTML과 다르게
        onChange Prop을 사용하면 입력 값이 바뀔 때마다 핸들러 함수를 실행합니다.
        oninput 이벤트와 같다고 생각하시면 되는데요.
        리액트 개발자들은 주로 onChange 라는 Prop을 사용하니까, 이 내용은 꼭 기억해주세요.
    • 폼을 다루는 기본적인 방법
      스테이트를 만들고 target.value 값을 사용해서 값을 변경해 줄 수 있었습니다.
      이때 value Prop으로 스테이트 값을 내려주고, onChange Prop으로 핸들러 함수를 넘겨줬는데요.
    function TripSearchForm() {
      const [location, setLocation] = useState('Seoul');
      const [checkIn, setCheckIn] = useState('2022-01-01');
      const [checkOut, setCheckOut] = useState('2022-01-02');
    
      const handleLocationChange = (e) => setLocation(e.target.value);
    
      const handleCheckInChange = (e) => setCheckIn(e.target.value);
    
      const handleCheckOutChange = (e) => setCheckOut(e.target.value);
    
      return (
        <form>
          <h1>검색 시작하기</h1>
          <label htmlFor="location">위치</label>
          <input id="location" name="location" value={location} placeholder="어디로 여행가세요?" onChange={handleLocationChange} />
          <label htmlFor="checkIn">체크인</label>
          <input id="checkIn" type="date" name="checkIn" value={checkIn} onChange={handleCheckInChange} />
          <label htmlFor="checkOut">체크아웃</label>
          <input id="checkOut" type="date" name="checkOut" value={checkOut} onChange={handleCheckOutChange} />
          <button type="submit">검색</button>
        </form>
      )
    }
    • 폼 값을 객체 하나로 처리하기
      이벤트 객체의 target.name 과 target.value 값을 사용해서 값을 변경해 줄 수도 있었습니다.
      이렇게하면 객체형 스테이트 하나만 가지고도 값을 처리할 수 있었죠.
    function TripSearchForm() {
      const [values, setValues] = useState({
        location: 'Seoul',
        checkIn: '2022-01-01',
        checkOut: '2022-01-02',
      })
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        setValues((prevValues) => ({
          ...prevValues,
          [name]: value,
        }));
      }
    
      return (
        <form>
          <h1>검색 시작하기</h1>
          <label htmlFor="location">위치</label>
          <input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={handleChange} />
          <label htmlFor="checkIn">체크인</label>
          <input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={handleChange} />
          <label htmlFor="checkOut">체크아웃</label>
          <input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={handleChange} />
          <button type="submit">검색</button>
        </form>
      )
    }
    • 기본 submit 동작 막기
      HTML 폼의 기본 동작은 submit 타입의 버튼을 눌렀을 때 페이지를 이동하는 건데요.
      이벤트 객체의 preventDefault 를 사용하면 이 동작을 막을 수 있었습니다.
    const handleSubmit = (e) => {
      e.preventDefault();
      // ...
    }
  • 📘파일 인풋
    파일 인풋은 반드시 비제어 인풋으로 만들어야 한다.
    파일 인풋의 value 속성은 사용자만 직접 바꿀 수 있다.(자바스크립트로 바꿀 때는 빈 문자열로만 바꿀 수 있다.)

ReviewForm.js

import { useState } from 'react';
import FileInput from './FileInput';
import './ReviewForm.css';

function ReviewForm() {
  const [values, setValues] = useState({
    title: '',
    rating: 0,
    content: '',
    imgFile: null,
  });

  const handleChange = (name, value) => {
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    handleChange(name, value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(values);
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <FileInput name="imgFile" value={values.imgFile} onChange={handleChange} />
      <input name="title" value={values.title} onChange={handleInputChange} />
      <input type="number" name="rating" value={values.rating} onChange={handleInputChange} />
      <textarea name="content" value={values.content} onChange={handleInputChange} />
      <button type="submit">확인</button>
    </form>
  );
}

export default ReviewForm;


FileInput.js

function FileInput({ name, value, onChange }) {
  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  return <input type="file" onChange={handleChange} />;
}

export default FileInput;
  • 📘ref로 DOM 노드 가져오기
import { useEffect, useRef } from 'react';

function FileInput({ name, value, onChange }) {
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  return <input type="file" onChange={handleChange} ref={inputRef} />;
}

export default FileInput;
  • 📘파일 인풋 초기화
    파일 인풋의 value 속성은 사용자만 직접 바꿀 수 있다.(자바스크립트로 바꿀 때는 빈 문자열로만 바꿀 수 있다.)
    value 속성을 빈 문자열로 바꿔주면 선택한 파일이 초기화된다.
import { useRef } from 'react';

function FileInput({ name, value, onChange }) {
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  return (
    <div>
      <input type="file" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;
  • 📘이미지 파일 미리보기
import { useEffect, useRef, useState } from 'react';

function FileInput({ name, value, onChange }) {
  const [preview, setPreview] = useState();
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  useEffect(() => {
    if (!value) return;
    const nextPreview = URL.createObjectURL(value);
    setPreview(nextPreview);
  }, []);

  return (
    <div>
      <img src={preview} alt="이미지 미리보기" />
      <input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;
  • 📘사이드 이펙트 정리하기
import { useEffect, useRef, useState } from 'react';

function FileInput({ name, value, onChange }) {
  const [preview, setPreview] = useState();
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  useEffect(() => {
    if (!value) return;
    const nextPreview = URL.createObjectURL(value);
    setPreview(nextPreview);

    return () => {
      setPreview();
      URL.revokeObjectURL(nextPreview);
    };
  }, [value]);

  return (
    <div>
      <img src={preview} alt="이미지 미리보기" />
      <input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;

//useEffect의 아래 부분은 디펜던시 리스트의 값이 바뀌어서 새로 콜백을 실행하게 될 텐데 
//새로 콜백을 실행하기 전에 리액트는 앞에서 리턴한 정리함수를 실행해서 사이드 이펙트를 정리할 수 있게 해준다.
// return () => {
//      setPreview();
//      URL.revokeObjectURL(nextPreview);
//    };
//  }, [value]);

📌데이터 보내기

  • 📘글 작성하기
// ReviewForm.js
import { useState } from 'react';
import { createReview } from '../api';
import FileInput from './FileInput';
import RatingInput from './RatingInput';
import './ReviewForm.css';

const INITIAL_VALUES = {
  title: '',
  rating: 0,
  content: '',
  imgFile: null,
};

function ReviewForm() {
  const [values, setValues] = useState(INITIAL_VALUES);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submittingError, setSubmittingError] = useState(null);

  const handleChange = (name, value) => {
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    handleChange(name, value);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('title', values.title);
    formData.append('rating', values.rating);
    formData.append('content', values.content);
    formData.append('imgFile', values.imgFile);
    try {
      setSubmittingError(null);
      setIsSubmitting(true);
      await createReview(formData);
    } catch (error) {
      setSubmittingError(error);
      return;
    } finally {
      setIsSubmitting(false);
    }
    setValues(INITIAL_VALUES);
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <FileInput
        name="imgFile"
        value={values.imgFile}
        onChange={handleChange}
      />
      <input name="title" value={values.title} onChange={handleInputChange} />
      <RatingInput
        name="rating"
        value={values.rating}
        onChange={handleChange}
      />
      <textarea
        name="content"
        value={values.content}
        onChange={handleInputChange}
      />
      <button disabled={isSubmitting} type="submit">
        확인
      </button>
      {submittingError && <div>{submittingError.message}</div>}
    </form>
  );
}

export default ReviewForm;


//api.js

const BASE_URL = 'https://learn.codeit.kr/api';

export async function getReviews({
  order = 'createdAt',
  offset = 0,
  limit = 6,
}) {
  const query = `order=${order}&offset=${offset}&limit=${limit}`;
  const response = await fetch(`${BASE_URL}/film-reviews?${query}`);
  if (!response.ok) {
    throw new Error('리뷰를 불러오는데 실패했습니다');
  }
  const body = await response.json();
  return body;
}

export async function createReview(formData) {
  const response = await fetch(`${BASE_URL}/film-reviews`, {
    method: 'POST',
    body: formData,
  });
  if (!response.ok) {
    throw new Error('리뷰를 생성하는데 실패했습니다.');
  }
  const body = await response.json();
  return body;
}
  • 📘리스폰스 데이터 반영하기
//ReviewForm.js
import { useState } from 'react';
import { createReview } from '../api';
import FileInput from './FileInput';
import RatingInput from './RatingInput';
import './ReviewForm.css';

const INITIAL_VALUES = {
  title: '',
  rating: 0,
  content: '',
  imgFile: null,
};

function ReviewForm({ onSubmitSuccess }) {
  const [values, setValues] = useState(INITIAL_VALUES);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submittingError, setSubmittingError] = useState(null);

  const handleChange = (name, value) => {
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    handleChange(name, value);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('title', values.title);
    formData.append('rating', values.rating);
    formData.append('content', values.content);
    formData.append('imgFile', values.imgFile);
    let result;
    try {
      setSubmittingError(null);
      setIsSubmitting(true);
      result = await createReview(formData);
    } catch (error) {
      setSubmittingError(error);
      return;
    } finally {
      setIsSubmitting(false);
    }
    const { review } = result;
    setValues(INITIAL_VALUES);
    onSubmitSuccess(review);
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <FileInput
        name="imgFile"
        value={values.imgFile}
        onChange={handleChange}
      />
      <input name="title" value={values.title} onChange={handleInputChange} />
      <RatingInput
        name="rating"
        value={values.rating}
        onChange={handleChange}
      />
      <textarea
        name="content"
        value={values.content}
        onChange={handleInputChange}
      />
      <button disabled={isSubmitting} type="submit">
        확인
      </button>
      {submittingError && <div>{submittingError.message}</div>}
    </form>
  );
}

export default ReviewForm;


// App.js
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import ReviewForm from './ReviewForm';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [loadingError, setLoadingError] = useState(null);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async (options) => {
    let result;
    try {
      setLoadingError(null);
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      setLoadingError(error);
      return;
    } finally {
      setIsLoading(false);
    }

    const { paging, reviews } = result;
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  const handleSubmitSuccess = (review) => {
    setItems((prevItems) => [review, ...prevItems]);
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewForm onSubmitSuccess={handleSubmitSuccess} />
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && (
        <button disabled={isLoading} onClick={handleLoadMore}>
          더 보기
        </button>
      )}
      {loadingError?.message && <span>{loadingError.message}</span>}
    </div>
  );
}

export default App;
  • 📘글 수정하기

  • 📘글 삭제하기

  • 📘리액트 Hook
    리액트가 제공하는 기능에 연결해서 그 값이나 기능을 사용하는 함수

    • useState
      리액트가 관리하는 State에 연결해서 변수처럼 값을 사용
    • useEffect
      내 콜백 함수를 리액트에 연결해서 렌더링 후에 함수 실행
    • useRef
      리액트가 관리하는 Ref 객체에 연결해서 current 값을 사용
  • 📘리액트 Hook의 규칙

    • 반드시 리액트 컴포넌트 함수나 커스텀 Hook 안에서 실행되어야 한다.

    • 반드시 함수의 최상위에서 실행해야 한다.(반복문이나 조건문 안에서 사용하면 안된다는 뜻이다.)

📌전역 데이터 다루기

  • 📘Context란?
    많은 컴포넌트에서 사용하는 데이터를 반복적인 Prop 전달(Prop Drilling) 없이 공유

  • 📘Context 만들기
    Context는 createContext 라는 함수를 통해 만들 수 있습니다.

import { createContext } from 'react';

const LocaleContext = createContext();

---------------------------------------------------
  
import { createContext } from 'react';

const LocaleContext = createContext('ko');
//이렇게 기본값을 넣어줄 수도 있다.
  • 📘Context 적용하기
    Context를 쓸 때는 반드시 값을 공유할 범위를 정하고 써야 하는데요,
    이때 범위는 Context 객체에 있는 Provider 라는 컴포넌트로 정해줄 수 있습니다.
    이때 Provider의 value prop으로 공유할 값을 내려주면 됩니다.
import { createContext } from 'react';

const LocaleContext = createContext('ko');

function App() {
  return (
    <div>
       ... 바깥의 컴포넌트에서는 LocaleContext 사용불가

       <LocaleContext.Provider value="en">
          ... Provider 안의 컴포넌트에서는 LocaleContext 사용가능
       </LocaleContext.Provider>
    </div>
  );
}
  • 📘Context값 사용하기
    useContext 라는 Hook을 사용하면 값을 가져와 사용할 수 있습니다.
    이때 아규먼트로는 사용할 Context를 넘겨주면 됩니다.
import { createContext, useContext } from 'react';

const LocaleContext = createContext('ko');

function Board() {
  const locale = useContext(LocaleContext);
  return <div>언어: {locale}</div>;
}

function App() {
  return (
    <div>
       <LocaleContext.Provider value="en">
          <Board />
       </LocaleContext.Provider>
    </div>
  );
}
  • 📘State, Hook 함께 활용하기
    Provider 역할을 하는 컴포넌트를 하나 만들고,
    여기서 State를 만들어서 value 로 넘겨줄 수 있습니다.
    그리고 아래의 useLocale 같이
    useContext 를 사용해서 값을 가져오는 커스텀 Hook을 만들 수도 있겠죠.
    이렇게 하면 Context에서 사용하는 State 값은
    반드시 우리가 만든 함수를 통해서만 쓸 수 있기 때문에
    안전한 코드를 작성하는데 도움이 됩니다.
import { createContext, useContext, useState } from 'react';

const LocaleContext = createContext({});

export function LocaleProvider({ children }) {
  const [locale, setLocale] = useState();
  return (
    <LocaleContext.Provider value={{ locale, setLocale }}>
      {children}
    </LocaleContext.Provider>
  );
}

export function useLocale() {
  const context = useContext(LocaleContext);

  if (!context) {
    throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
  }

  const { locale } = context;
  return locale;
}

export function useSetLocale() {
  const context = useContext(LocaleContext);

  if (!context) {
    throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
  }

  const { setLocale } = context;
  return setLocale;
}
  • 📘
  • 📘
  • 📘
profile
누구나 같은 팀으로 되길 바라는 개발자가 되자

0개의 댓글