실전! API데이터 연동-공유-관리

실전! API데이터 연동-공유-관리

유형
실습문서
주제

레시피앱제작

순번
9
태그
설명

Router-loader, UseContext 활용

0. 준비

0.1. 시작파일

💡

이전 파일이 없을 경우 아래의 파일을 다운로드 하여 진행한다

start.zip23.3KB
파일활용방법안내

0.2. 연관링크

종류
🔗링크
리액트라우터 공식문서
리액트 공식문서

0.3. 영상

1. .env

1.1. 환경변수의 이해

.env 는 environment 의 약자로 환경 이라는 의미이다.

애플리케이션의 환경 변수를 환경 변수를 정의하고 관리하는 파일이다

1.1.1. 기능

image
  1. 환경변수관리 .env 파일의 핵심 기능은 애플리케이션의 환경 변수를 정의하고 관리하는 것이다. API 키, 데이터베이스 연결 정보, 서버 URL 등의 값을 관리하며, 개발 환경별로 다르게 설정할 수도 있다.
  1. 보안강화 민감한 정보를 소스 코드에 직접 넣지 않고, 환경 변수로 관리함으로써 보안을 강화할 수 있다. 하지만 .env 파일은 버전 관리 시스템에서 반드시 제외해야 한다.
  1. 설정의 유연성 같은 코드로 여러 환경에 맞게 앱을 설정할 수 있다. 예를 들어, 레시피 앱을 여러 클라이언트에게 제공한다면 소스 코드는 같지만 서비스 키는 달라야 할것이다. 이때 .env를 사용하면 쉽게 설정을 바꿀수있어 유연성이 높아진다.

1.1.2. 주의사항

image

아직 .env 사용이 익숙치 않을 경우 가장 많이 하는 실수이고 가장 치명적인 실수 이니 반드시 점검하자.

  1. 파일 생성전 반드시 .gitignore 에 추가할것
  2. React 프로젝트에서는 변수명의 접두어가 지정되어 있으니 유의할것
  3. React_APP_변수명 #변수명은 영문 대문자를 추천함

  4. .env 파일 수정후에는 반드시 서버 재부팅!!!

1.2.코딩하기

1.2.1. 준비

  1. 시작파일 내려받아 압축을 푼다.
  2. 작업중인 리액트 앱을 열고 시작파일로 덮어 씌우고 VSCode 를 실행하여 코딩을 시작한다.
  3. 개발서버 실행

1.2.2. gitignore

  1. .gitignore 파일을 연다.
  2. .env 를 추가한다.
  3. # 환겅설정 추가
    .env

1.2.3. 환경변수 생성

  1. 프로젝트의 루트경로에 .env 파일 생성후 아래와 같이 작성한다.
  2. # 을 하면 주석이다.
    # '내 서비스키' 에 본인의 레시피 API 서비스키를 넣는다 
    # 예시) REACT_APP_KEY='o8dw9b42270a44'
    
    REACT_APP_KEY='내서비스키'
    image

1.2.4. 환경변수 사용

  1. src/App.js 로 이동한다.
  2. getDB 함수의 표시된 부분을 환경변수로 변경하자
  3. image
    function App() {
      const KEY = process.env.REACT_APP_KEY;
          try {
          const { data } = await axios.get(`http://openapi.foodsafetykorea.go.kr/api/${KEY}/COOKRCP01/json/1/5`);
    //...
    image
🐨

.env 파일에는 이외에도 다양한 환경변수를 추가할수 있다.

2. API 데이터 연동하고 관리하기

2.1. 라우터 설정하기

(❁´◡`❁) 모듈 설치

npm i react-router-dom

2.1.1. App.js

App 컴포넌트는 리액트 앱에서 Entry Point (시작점) 역할을 하며 최상위 컴포넌트이다.

전체 애플리케이션의 구조와 흐름이 시작되는 지점이므로 UI요소 보다는 데이터의 흐름을 제어할수 있는 로직으로 구성하는 것이 유리하다.

레시피 앱의 라우터 설정은 App.js 에 작성하도록 한다.

  1. createBrowserRouter , RouterProvider
  2. import { createBrowserRouter, RouterProvider } from "react-router-dom";
    //...
    const router = createBrowserRouter([
      { path: '/', element: '<ListGroup/>' }
    ]);
    <RouterProvider router={router} />
    //...
    function App() {

2.1.2. 레시피 앱 구조 변경 준비

App 컴포넌트는 라우터 설정만을 관리할 것이므로 UI 렌더링 로직은 별도의 컴포넌트로 분리 할 것이다.
  1. index.js 수정
  2. import React from "react";
    import ReactDOM from "react-dom/client";
    import "./index.css";
    import App from "./App";
    
    
    const root = ReactDOM.createRoot(document.querySelector("#root"));
    root.render(<App />);
    
    모든 코드를 삭제하고 App컴포넌트만 렌더한다.
  3. pages 폴더의 모든 파일 삭제
    1. pages 폴더 하위에 파일이 존재할 경우 모두 삭제한다.
    2. image

2.1.3. 리액트 앱 구조 이해

레시피 앱의 프로젝트 구조를 살펴보자.

표시된 파일을 추가하고 h1에 컴포넌트 명을 리턴하는 로직을 작성한다.

src
├── index.js
├── App.js
├── components
│   ├── Blocks.jsx
│   └── Navi.jsx
└── pages
    ├── Category.jsx
    ├── Detail.jsx
    ├── Home.jsx
    └── Root.jsx
  • 각 파일별 역할은 다음과 같다
  • 파일명
    설명
    index.js
    애플리케이션의 진입 파일
    App.js
    라우터 설정을 담당하는 컴포넌트
    Blocks.
    List 와 Title 을 표시하는 컴포넌트
    Navi.js
    GNB 컴포넌트
    Detail.js
    레시피 상세 정보를 표시하는 페이지
    Root.js
    최상위 경로를 관리하는 컴포넌트
    Category.js
    카테고리를 표시하는 페이지
    Home.js
    메인화면
  • pages/Home.js
  • const Home = () => {
      return (
        <>
          <h1>Home</h1>
        </>
      );
    };
    export default Home;

2.1.4. App.js

App 컴포넌트로 이동하여 코드를 수정한다.

App 컴포넌트는 라우터 설정을 관리할 것이다.

라우터 설정외 다른 소스 코드들은 삭제 할 예정이다.

🐨 App.js

  1. Root 컴포넌트를 렌더한다
  2. //컴포넌트
    import Root from "./pages/Root";
  3. router 설정
  4. const router = createBrowserRouter([{ path: "/", element: <Root /> }]);
  5. 라우터를 연결할 컴포넌트를 모두 임포트 한후 라우터를 작성한다.
  6. import Home from "./pages/Home";
    import Category from "./pages/Category";
    import Detail from "./pages/Detail";
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <Root />,
        children: [
          { index: true, element: <Home /> },
          { path: 'category', element: <Category /> },
          { path: ':id', element: <Detail /> },
        ],
      },
    ]);
    //...중략
  7. router 설정 연결 App컴포넌트의 return에 Fragment 를 추가하고 div 요소와 형제로 RouterProvider를 작성한다.
  8. function App() {
    // ... 중략
      return (
        <>
          <RouterProvider router={router} />
    	     <div className="inner">
    // ... 중략
    	    </>
      );
    }
    
    export default App;

2.1.5. pages/Root

pages 폴더 내의 컴포넌트를 수정한다.
  • outlet과 Navi 를 추가한다.

🐨 Root.js

import { Outlet } from "react-router-dom";
import Navi from "../components/Navi";
const Root = () => {
  return (
    <>
	    <h1>Root</h1>
      <Navi />
      <Outlet />
    </>
  );
};
export default Root;

2.1.6. components/Navi.js

NavLink 컴포넌트를 사용하여 GNB기능을 구현한다.

NavLink는 자동활성화 표시 기능이 포함된 Link 컴포넌트이다.

🐨 components/Navi.js

  1. Navi jsx 추가 후 Link 컴포넌트를 사용하여 홈과 카테고리 컴포넌트로 연결한다.
  2. import { Link } from 'react-router-dom';
    
    const Navi = (props) => {
      return (
        //nav>.inner>Link*2
        <nav>
          <div className='inner'>
            <Link to='/' end>메인화면</Link>
            <Link to='category'>카테고리</Link>
          </div>
        </nav>
      );
    };
    export default Navi;
    
  1. Link 를 NaviLink 로 변경한다.
  2. import { NavLink } from "react-router-dom";
    const Navi = (props) => {
      return (
        <nav>
          <div className="inner">
            <NavLink to="/"> 메인화면</NavLink>
            <NavLink to="category"> 카테고리</NavLink>
          </div>
        </nav>
      );
    };
    export default Navi;
    
  3. Navi 의 활성화된 링크에 class 가 추가되어 있다. 이는 우리가 작성한 것이 아니고 NavLink 컴포넌트의 기능이다. 우리는 css 파일에 .active 일때 변경할 디자인만 작성하면 된다.
  4. image
  1. active 말고 나만의 클래스를 추가해보자.
  2. <NavLink to="/" className={({ isActive }) => (isActive ? "on" : "")}>
      메인화면
    </NavLink>
    <NavLink to="category" className={({ isActive }) => (isActive ? "on" : "")}>
      카테고리
    </NavLink>

    .on 에 대한 css를 추가하면 렌더되는 것을 확인할수 있다.

  3. end 속성은 경로가 완전히 일치할때 클래스를 추가한다.
  4. <NavLink to="/" end> 메인화면</NavLink>

2.2. 라우터로 데이터 패칭하고 처리하기 (loader)

react-rotuer-dom의 loader로 데이터 패칭하기.
image

2.2.1. App.js

Home과 App 컴포넌트를 열고 분할창으로 작업한다.

Home의 getDB 함수의 소스코드 중 부분을 App 컴포넌트로 옮길 것이다.

  1. App .js 의 라우터 속성 추가
  2. image
  3. aync 키워드 작성후 함수 작성
  4. App 의 환경변수를 Home.js으로 복붙
  5. image
  6. App 의 getDB 의 try~catch 를 잘라 Home.js에 붙이기
  7. image
  8. App, Home 모두 useEffect, useState 연관 로직 삭제
  9. image
  10. App 의 .inner 블록 잘라내기
  11. image

2.2.2. Home.js 수정

  1. 앞서 잘라낸 코드 return문에 붙이기
  2.   return (
        <div className='inner'>
          {loading.state ? (
            <h1>로딩중입니다...</h1>
          ) : (
            <>
              <Title h={1} title={'코알라 레시피'} />
              <Title h={2} title={loading.data[0].RCP_PAT2} />
              <List data={loading.data} />
            </>
          )}
        </div>
      );
    image
  3. useEffect, useState 와 필요없는 코드 모두 삭제
const KEY = process.env.REACT_APP_KEY;
const Home = () => {
  const getDB = async () => {
    try {
      const { data } = await axios.get(
        `http://openapi.foodsafetykorea.go.kr/api/${KEY}/COOKRCP01/json/1/5`
      );
      const {
        COOKRCP01: { row },
      } = data;
    } catch (error) {
      console.error(error);
    }
  };
  getDB();

  return (
    <div className='inner'>
      <Title h={1} title={'코알라 레시피'} />
      <Title h={2} title={row[0].RCP_PAT2} />
      <List data={row} />
    </div>
  );
};

export default Home;

2.2.3. loader 작성 - App.js

  1. Home.jsx의 표시된 부분을 잘라내여 App.js 의 표시된 부분에 붙여넣는다.
  2. image
  3. App 에서 콘솔로그에 row 변수에 주입된 값을 확인한다.
  4. import { createBrowserRouter, RouterProvider } from 'react-router-dom';
    import axios from 'axios';
    
    import { List, Title } from './components/Blocks';
    // 수정
    import Root from './pages/Root';
    import Home from './pages/Home';
    import Category from './pages/Category';
    import Detail from './pages/Detail';
    const KEY = process.env.REACT_APP_KEY;
    // router setting ////////////////////
    const router = createBrowserRouter([
      {
        path: '/',
        element: <Root />,
        children: [
          {
            index: true,
            element: <Home />,
            loader: async () => {
              try {
                const { data } = await axios.get(`http://openapi.foodsafetykorea.go.kr/api/${KEY}/COOKRCP01/json/1/5`);    
                const {
                  COOKRCP01: { row },
                } = data;
                console.log(row);            
              } catch (error) {
                console.error(error);
              }
            },
          },
          { path: 'category', element: <Category /> },
          { path: ':id', element: <Detail /> },
        ],
      },
    ]);
    
    function App() {
      return (
        <>
          <RouterProvider router={router} />
        </>
      );
    }
    
    export default App;
    

image

router 과정에서 loader 함수를 사용하여 데이터를 연결하였다.

loader은 컴포넌트 렌더 전 라우팅 단계에서 실행되므로 useState , useEffect 훅이 없어도 필요한 컴포넌트에서 바로 사용할수 있다.

loader 함수에 저장된 데이터를 외부에서 사용할수 있도록 return 한다.

row 를 리턴한다.
row 를 리턴한다.

2.2.4. useLoaderData - Home.jsx

loader 함수를 사용하여 전달받은 데이터는 라우터의 path 경로에 연결된 컴포넌트에서 가져와 사용할수 있다. 이때 useLoaderData 훅을 사용한다.

🐨 Home.jsx

image

콘솔창으로 data 의 값 확인

import { useLoaderData } from 'react-router-dom';
import { Title, List } from '../components/Blocks';

const KEY = process.env.REACT_APP_KEY;
const Home = () => {
  const data = useLoaderData();
  console.log(data);

  return (
    <div className='inner'>
      <Title h={1} title={'코알라 레시피'} />
      <Title h={2} title={data[0].RCP_PAT2} />
      <List data={data} />
    </div>
  );
};

export default Home;
image

2.2.5. useNavigation()

현재 router의 상황을 알려주는 훅이다.

이 훅을 사용하여 router 설정 단계에서 바로 사용자에게 지연로딩 등의 피드백을 전달할수 있다.

주요상태 (state)

  • "idle": 이동 없음. (페이지가 멈춰있는 평소의 상태)
  • "loading": 전환 중. (페이지가 이동하면서 로딩중 상태)

우리 앱에서 카테고리에서 홈으로 이동해보자.

홈 카테고리는 서버에서 데이터를 읽어와야 하는 시간이 필요하므로 카테고리의 렌더링 시간보다 오래 걸린다.

이때 사용자에게 데이터를 불러오는 중이라는 메시지를 띄워보도록 하자.

🐨 Root.jsx

  1. useNavigation 훅을 임포트후 반환값을 확인한다.
  2. import { Outlet, useNavigation } from 'react-router-dom';
    import Navi from '../components/Navi';
    const Root = () => {
      console.log(useNavigation());
    //...
    state 키의 값을 사용하면 상태를 표시할수 있다.
    state 키의 값을 사용하면 상태를 표시할수 있다.
  3. 반환값을 활용한다.
  4. import { Outlet, useNavigation } from 'react-router-dom';
    import Navi from '../components/Navi';
    const Root = () => {
      console.log(useNavigation());
      const loading = useNavigation();
    
      return (
        <>
          <Navi />
          {loading.state === 'loading' && (
            <p style={{ position: 'fixed', top: '20%', left: '25%', fontSize: '3em' }}>
              로딩중입니다..
            </p>
          )}
          <Outlet />
        </>
      );
    };
    export default Root;

2.2.7. 동적라우트, loader

:id 에도 loader 를 작성하여 레시피 디테일 컴포넌트를 구현해보자

:id 와 같이 동적경로 일 경우 우리는 useParams 훅을 사용하여 해당 데이터를 전달받을 수 있었다.

loader 함수를 사용할 경우 어떤 점이 다른지 알아보자.

🐨 Detail.jsx

  1. Detail 컴포넌트에 useParams 를 사용하여 url 의 파라미터로 전달되는 데이터를 저장하고 출력해본다
  2. import { useParams } from 'react-router-dom';
    const Detail = () => {
      const data = useParams();
      console.log(data);
      
      return (
        <>
          <h1>Detail</h1>
        </>
      );
    };
    export default Detail;
    
    image
  1. loader 를 사용할 경우 useLoaderData 를 사용할수 있다.
  2. import { useLoaderData } from 'react-router-dom';
    function Detail() {
      const id = useLoaderData();
      console.log(id);
      //...
    image
🐨

홈→List comp→Detail Comp

홈화면의 레시피 목록 중 하나를 클릭하면 클릭된 데이터의 REP_NM 속성을 추출한다.

List 컴포넌트에서 useNavigate 를 사용하여 RCP_NM 의 경로로 이동하는 로직을 작성한다.

주소창에는 RCP_NM 이 표시되며 이는 router 에서 :id 경로로 인식되어 Detail 컴포넌트로 연결 된다.

router 에서 :id 패스 설정에 loader 를 추가로 작성하고 RCP_NM 을 파라미터로 전달받아 Detail 컴포넌트에는 선택된 RCP_NM 의 데이터를 받아올수 있도록 구성한다.

🐨 List 컴포넌트

const List = ({ data }) => {
  const navigate = useNavigate();
  const handleClick = (RCP_NM) => {
    navigate(`/${RCP_NM}`);
  };

  return (
    <div className='group'>
      {data.map(({ RCP_SEQ, RCP_NM, ATT_FILE_NO_MAIN, RCP_WAY2 }) => (
        <div key={RCP_SEQ} className='list' onClick={() => handleClick(RCP_NM)}>
          <img src={ATT_FILE_NO_MAIN} alt={RCP_NM} />
          <div className='list-txt-wrap'>
            <div className='list-txt-title'>{RCP_NM}</div>
            <div className='list-txt-way'>{RCP_WAY2}</div>
          </div>
        </div>
      ))}
    </div>
  );
};

🐨 App.js

const router = createBrowserRouter([
//...
	{
	  path: ':id',
	  element: <Detail />,
	  loader: async ({ request, params }) => {
	    try {
	      const { data } = await axios.get(`http://openapi.foodsafetykorea.go.kr/api/${KEY}/COOKRCP01/json/1/1/RCP_NM=${params.id}`);
	      return data.COOKRCP01.row[0];
	    } catch (error) {
	      console.error(error);
	    }
	  },
	},
//...

🐨 Detail

import React from 'react';
import { useLoaderData } from 'react-router-dom';
import { Title } from '../components/Blocks';
function Detail() {
  const data = useLoaderData();
  return (
    <div className='inner'>
      <div className='detail'>
        <Title h={1} title={data.RCP_NM} />
        <img src={data.ATT_FILE_NO_MAIN} alt={data.RCP_NM} />
        <p>{data.RCP_PARTS_DTLS}</p>
        <p>{data.RCP_WAY2}</p>
        <p>{data.MANUAL01}</p>
      </div>
    </div>
  );
}
export default Detail;
조리순서별 이미지와 메뉴얼은 동영상 강좌에서 자세히 다루지 않는다. map을 사용하여 직접 렌더링 해보고 추후 제공된 완성파일을 비교하여 분석해보자.

2.3. Category 구현

레시피API 의 데이터를 살펴보면 요리종류가 저장된 속성인 RCP_PAT2 가 있다.

라우터에서 Category 컴포넌트에 해당 파라미터와의 API통신을 연결하고 전달받은 데이터를 필터링하여 레시피를 카테고리별로 나누에 렌더링 시켜보자.

🐨 App.js

  1. category 패스에 loader 를 작성한다.
  2. {
      path: 'category',
      element: <Category />,
      loader: async () => {
        try {
          const { data } = await axios.get(`http://openapi.foodsafetykorea.go.kr/api/${KEY}/COOKRCP01/json/1/50`);
          const {
            COOKRCP01: { row },
          } = data;
    
          return row;
        } catch (error) {
          console.error(error);
        }
      },
    },

🐨 Category.jsx

import { useLoaderData } from 'react-router-dom';
import { List, Title } from '../components/Blocks';
const Category = (props) => {
  const data = useLoaderData();
  //data.RCP_PAT2 의 중복값을 제거후 categories 배열에 할당
  const categories = [...new Set(data.map((data) => data.RCP_PAT2))];
  return (
    <>
      <div className='inner'>
        <Title h={1} title={'카테고리'} />
        {categories.map((category, index) => {
          const groupedData = data.filter((el) => el.RCP_PAT2 === category);

          return (
            <div key={index}>
              <Title h={2} title={category} />
              <List data={groupedData} />
            </div>
          );
        })}
      </div>
    </>
  );
};
export default Category;

여기까지 실행하면 레시피 앱이 예쁘게 렌더링 된다.

3. useContext()

리액트의 Context 기능은 프로그램에서 자주 사용해야 하는 값을 접근하기 쉬운 곳에 저장해놓고 필요할때마다 꺼내어 쓸수 있게 하는 기능이다.

주로 색상설정 이나 사용자의 로그인 상태 처럼 컴포넌트의 흐름과 무관하게 전역적으로 사용해야 하는 값 에 쓴다.

컬러 테마를 만들어 보자.

3.1. 저장소 생성

로컬 디렉토리에 ThemeContext.js 파일을 생성후 코드를 작성한다.

import { createContext, useState,} from 'react';

// ThemeContext 생성
export const ThemeContext = createContext();

// ThemeProvider 컴포넌트 - 전역 색상 테마 관리자
export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  // 테마를 토글하는 함수
  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };
	// ThemeContext의 Provider 속성으로 저장소에 접근하는 컴포넌트가 theme 의 값을 사용할수 있게 연결한다.
  return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
};

3.2. useContext()

Home 컴포넌트로 이동하여 context 에 접근해 보자.

Home 은 Root 컴포넌트에서 임포트 되었으므로 context 없이 접근할 경우 Root 컴포넌트로 부터 컬러 테마 속성을 주입받아야 한다.

하지만 context 를 사용할 경우 컴포넌트간의 의존관계와 무관하게 필요한 값을 사용할수 있으므로 바로 컬러 테마를 조정할수 있다.

*중복코드 생략함

🐨 Home.jsx

import { useContext } from 'react';
import { ThemeContext } from '../ThemeContext';

const Home = () => {
  const data = useLoaderData();
  const { theme, toggleTheme } = useContext(ThemeContext);
	 return (
    <div className={theme}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <div className='inner'>
        <Title h={1} title={'코알라 레시피'} />
        <List data={data} />
      </div>
    </div>
  );
  

이제 홈 화면에 테마 버튼이 렌더되며 클릭시 색상테마가 변경된다.

다른 컴포넌트에도 적용해보자

4. 완성파일

09.zip27.2KB