분류 Reactjs

React Hooks와 Context API를 사용하여 간단한 Pokémon Web App을 빌드하는 방법

컨텐츠 정보

  • 조회 410 (작성일 )

본문

루비, 파이썬, 바닐라 자바 ​​스크립트를 사용하여 7 년 동안 풀 스택을 개발 한 후 요즘에는 주로 자바 스크립트, 타입 스크립트, 리 액트 및 리덕스를 다루고 있습니다.


https://www.freecodecamp.org/news/building-a-simple-pokemon-web-app-with-react-hooks-and-context-api/ 


JavaScript 커뮤니티는 훌륭하고 정말 빠르게 움직입니다. 많은 것들이 "밤새", 일반적으로 비 유적으로, 때로는 문자 그대로 만들어집니다. 이 모든 것이 최신 상태를 유지하기가 정말로 어렵습니다.


저는 항상 JavaScript 파티에 늦었다 고 생각합니다. 그리고 나는 파티를 정말로 좋아하지 않지만 거기에 가고 싶습니다.


React 및 Redux와 함께 일한 지 1 년 만에 상태를 관리하기 위해 Hooks 및 Context API와 같은 새로운 것을 배워야 한다고 생각했습니다. 그것에 관한 몇 가지 기사를 읽은 후에 나는 이러한 개념을 시험 해보고 싶었습니다. 그래서 실험실로 그러한 것들을 실험 할 간단한 프로젝트를 만들었습니다.


내가 어렸을 때부터 포켓몬에 대한 열정을 가지고 있었습니다. 게임 보이에서 게임을 하고 모든 리그를 정복하는 것은 항상 재미 있었습니다. 이제 개발자로서 Pokémon API를 가지고 놀고 싶습니다.


페이지의 다른 부분에서 데이터를 공유 할 수 있는 간단한 웹 페이지를 작성하기로 결정했습니다. 이 페이지에는 세 가지 주요 섹션이 있습니다.


  • 기존의 모든 포켓몬 목록이 있는 상자
  • 포획 된 모든 포켓몬 목록이 있는 상자
  • 새로운 포켓몬을 목록에 추가 할 수 있는 입력 상자


그리고 각 상자에는 다음과 같은 동작 또는 동작이 있습니다.


  • 첫 번째 상자에 있는 각 포켓몬에 대해 두 번째 상자를 캡처하여 보낼 수 있습니다
  • 두 번째 상자에 있는 각 포켓몬을 풀어서 첫 번째 상자로 보낼 수 있습니다
  • 게임 신으로서, 입력 내용을 채우고 첫 번째 상자로 보내 포켓몬을 만들 수 있습니다.

그래서 제가 구현하고자 하는 모든 기능은 분명했습니다. 목록과 작업입니다.


포켓몬 목록 


내가 먼저 만들고 싶었던 기본 기능은 포켓몬을 나열하는 것입니다. 따라서 객체 배열의 경우 각 객체의 이름 속성을 나열하고 표시하고 싶었습니다.


나는 첫 번째 상자 인 기존 포켓몬으로 시작했습니다.


처음에는 Pokémon API가 필요 없다고 생각했습니다. 목록을 조롱하고 작동하는지 확인할 수 있었습니다. useState를 사용하면 구성 요소 상태를 선언하고 사용할 수 있습니다.


우리는 그것을 테스트하기 위해 모의 포켓몬 목록의 기본값으로 정의합니다.


const [pokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

여기에 3 개의 포켓몬 개체 목록이 있습니다. useState 후크는 현재 상태와 이 작성된 상태를 업데이트 할 수 있는 기능의 한 쌍의 항목을 제공합니다.


이제 포켓몬의 상태를 통해 이를 매핑하고 각각의 이름을 렌더링 할 수 있습니다.


{pokemons.map((pokemon) => <p>{pokemon.name}</p>)}

각 포켓몬의 이름을 단락 태그로 반환하는지도 일 뿐입니다.


이것은 전체 구성 요소입니다.


import React, { useState } from 'react';

const PokemonsList = () => {
  const [pokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <p>{pokemon.id}</p>
          <p>{pokemon.name}</p>
        </div>)}
    </div>
  )
}

export default PokemonsList;

여기에 약간의 조정이 있습니다.


  • 포켓몬의 아이디와 이름을 조합하여 키를 추가했습니다.
  • 또한 id 속성에 대한 단락을 렌더링 했습니다 (방금 테스트 한 것이지만 나중에 제거하겠습니다).

큰! 이제 첫 번째 목록이 만들어졌습니다.


나는 이 같은 구현을 만들고 싶지만 지금은 캡처 된 포켓몬을 위해. 그러나 포획 된 포켓몬에 대해서는 먼저 "게임"이 시작될 때 포획 된 포켓몬이 없기 때문에 빈 목록을 만들고 싶습니다.


const [pokemons] = useState([]);

그게 정말 간단합니다!


전체 구성 요소는 다른 구성 요소와 유사합니다.


import React, { useState } from 'react';

const CapturedPokemons = () => {
  const [pokemons] = useState([]);

  return (
    <div className="pokedex">
      <h2>Captured Pokemons</h2>

      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <p>{pokemon.id}</p>
          <p>{pokemon.name}</p>
        </div>)}
    </div>
  )
}

export default CapturedPokemons;

여기서는 map을 사용하지만 배열이 비어 있으면 아무것도 렌더링 하지 않습니다.


이제 두 가지 주요 구성 요소가 있으므로 App 구성 요소에서 함께 사용할 수 있습니다.


import React from 'react';
import './App.css';

import PokemonsList from './PokemonsList';
import Pokedex from './Pokedex';

const App = () => (
  <div className="App">
    <PokemonsList />
    <Pokedex />
  </div>
);

export default App;

캡처 및 해제 


이것은 우리가 포켓몬을 캡처하고 릴리스 할 수 있는 앱의 두 번째 부분입니다. 예상되는 동작을 살펴 보겠습니다.


사용 가능한 포켓몬 목록에 있는 각 포켓몬에 대해 액션을 사용하여 포켓몬을 캡처 하고 싶습니다. 포획 행동은 그들이 있던 목록에서 그것들을 제거하고 포획 된 포켓몬의 목록에 추가합니다.


해제 동작은 비슷한 동작을 합니다. 그러나 사용 가능한 목록에서 캡처 된 목록으로 이동하는 대신 그 반대가 됩니다. 캡처 된 목록에서 사용 가능한 목록으로 이동합니다.


따라서 두 상자 모두 포켓몬을 다른 목록에 추가하려면 데이터를 공유해야 합니다. 앱에서 다른 구성 요소이므로 어떻게 해야 합니까? React Context API에 대해 이야기합시다.


컨텍스트 API는 정의 된 React 컴포넌트 트리에 대한 글로벌 데이터를 작성하도록 설계되었습니다. 데이터가 전역 적이므로 이 정의 된 트리의 구성 요소간에 데이터를 공유 할 수 있습니다. 두 상자 사이에 간단한 포켓몬 데이터를 공유하는 데 사용하겠습니다.


참고 사항 : "컨텍스트는 기본적으로 여러 중첩 수준의 여러 구성 요소에서 일부 데이터에 액세스해야 할 때 사용됩니다." -React Docs.


API를 사용하여 간단히 다음과 같은 새로운 컨텍스트를 만듭니다.


import { createContext } from 'react';

const PokemonContext = createContext();

이제 PokemonContext를 사용하면 공급자를 사용할 수 있습니다. 구성 요소 트리의 구성 요소 래퍼로 작동합니다. 이 구성 요소에 전역 데이터를 제공하고 이 컨텍스트와 관련된 모든 변경 사항을 구독 할 수 있습니다. 다음과 같이 보입니다 :


<PokemonContext.Provider value={/* some value */}>

값 prop은이 컨텍스트가 랩핑 된 컴포넌트를 제공하는 값일 뿐입니다. 사용 가능한 목록과 캡처 된 목록에 무엇을 제공해야 합니까?

  • pokemons : 사용 가능한 목록에 나열
  • capturedPokemons: 캡처 된 목록에 나열
  • setPokemons: 사용 가능한 목록을 업데이트 할 수 있도록
  • setCapturedPokemons: 캡처 된 목록을 업데이트 할 수 있도록

앞에서 useState 부분에서 언급했듯이 이 후크는 항상 쌍과 상태를 업데이트하는 함수를 제공합니다. 이 함수는 컨텍스트 상태를 처리하고 업데이트합니다. 즉, setPokemons 및 setCapturedPokemons입니다. 어떻게?


const [pokemons, setPokemons] = useState([
  { id: 1, name: 'Bulbasaur' },
  { id: 2, name: 'Charmander' },
  { id: 3, name: 'Squirtle' }
]);

이제 setPokemons가 있습니다.


const [capturedPokemons, setCapturedPokemons] = useState([]);

이제 setCapturedPokemons도 있습니다.


이러한 모든 값을 갖추면 이제 제공자의 가치 제안에 전달할 수 있습니다.


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

export const PokemonContext = createContext();

export const PokemonProvider = (props) => {
  const [pokemons, setPokemons] = useState([
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ]);

  const [capturedPokemons, setCapturedPokemons] = useState([]);

  const providerValue = {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  };

  return (
    <PokemonContext.Provider value={providerValue}>
      {props.children}
    </PokemonContext.Provider>
  )
};

이 모든 데이터와 API를 랩핑하여 컨텍스트를 작성하고 정의 된 값으로 컨텍스트 제공자를 리턴하도록 PokemonProvider를 작성했습니다.


그러나 이 모든 데이터와 API를 어떻게 구성 요소에 제공합니까? 우리는 두 가지 중요한 일을 해야 합니다


  • 이 컨텍스트 프로 바이더에 컴퍼넌트를 랩합니다
  • 각 구성 요소에서 컨텍스트 사용


먼저 감싸도록 하겠습니다 :


const App = () => (
  <PokemonProvider>
    <div className="App">
      <PokemonsList />
      <Pokedex />
    </div>
  </PokemonProvider>
);

그리고 useContext를 사용하고 생성 된 PokemonContext를 전달하여 컨텍스트를 사용합니다. 이처럼 :


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

useContext(PokemonContext); // returns the context provider value we created

사용 가능한 포켓몬을 잡을 수 있기를 원하므로 setCapturedPokemons 함수 API가 캡처 된 포켓몬을 업데이트 하도록 하는 것이 유용합니다.


각 포켓몬이 캡처되면 사용 가능한 목록에서 포켓몬을 제거해야 합니다. setPokemons도 여기에 필요합니다. 그리고 각 목록을 업데이트하려면 현재 데이터가 필요합니다. 따라서 기본적으로 컨텍스트 제공자의 모든 것이 필요합니다.


포켓몬을 잡기 위한 액션이 있는 버튼을 만들어야 합니다 :


  • 캡처 기능을 호출하고 포켓몬을 전달하는 onClick이 있는 <button> 태그
<button onClick={capture(pokemon)}>+</button>

캡처 기능은 포켓몬과 캡처 된 포켓몬 목록을 업데이트합니다


const capture = (pokemon) => (event) => {
  // update captured pokemons list
  // update available pokemons list
};

캡처 된 포켓몬을 업데이트 하기 위해 현재 캡처 된 포켓몬 및 포켓몬과 함께 setCapturedPokemons 함수를 호출하면 됩니다.

setCapturedPokemons([...capturedPokemons, pokemon]);

그리고 포켓몬 목록을 업데이트하려면 캡처 할 포켓몬을 필터링 하십시오.


setPokemons(removePokemonFromList(pokemon));

removePokemonFromList는 캡처 된 포켓몬을 제거하여 포켓몬을 필터링 하는 간단한 기능입니다.


const removePokemonFromList = (removedPokemon) =>
  pokemons.filter((pokemon) => pokemon !== removedPokemon)

이제 구성 요소가 어떻게 보입니까?


import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

export const PokemonsList = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons
  } = useContext(PokemonContext);

  const removePokemonFromList = (removedPokemon) =>
    pokemons.filter(pokemon => pokemon !== removedPokemon);

  const capture = (pokemon) => () => {
    setCapturedPokemons([...capturedPokemons, pokemon]);
    setPokemons(removePokemonFromList(pokemon));
  };

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <div>
            <span>{pokemon.name}</span>
            <button onClick={capture(pokemon)}>+</button>
          </div>
        </div>)}
    </div>
  );
};

export default PokemonsList;

포획 된 포켓몬 구성 요소와 매우 유사합니다. 캡처 대신 릴리스 함수가 됩니다.


import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const CapturedPokemons = () => {
  const {
    pokemons,
    setPokemons,
    capturedPokemons,
    setCapturedPokemons,
  } = useContext(PokemonContext);

  const releasePokemon = (releasedPokemon) =>
    capturedPokemons.filter((pokemon) => pokemon !== releasedPokemon);

  const release = (pokemon) => () => {
    setCapturedPokemons(releasePokemon(pokemon));
    setPokemons([...pokemons, pokemon]);
  };

  return (
    <div className="captured-pokemons">
      <h2>CapturedPokemons</h2>

      {capturedPokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <div>
            <span>{pokemon.name}</span>
            <button onClick={release(pokemon)}>-</button>
          </div>
        </div>)}
    </div>
  );
};

export default CapturedPokemons;

복잡성 감소 


이제 useState 후크, 컨텍스트 API 및 컨텍스트 제공자 useContext를 사용합니다. 그리고 더 중요한 것은 포켓몬 상자간에 데이터를 공유 할 수 있다는 것입니다.


상태를 관리하는 다른 방법은 useState의 대안으로 useReducer를 사용하는 것입니다.


감속기 수명주기는 다음과 같이 작동합니다. useReducer는 디스패치 기능을 제공합니다. 이 함수를 사용하면 컴포넌트 내부에 액션을 전달할 수 있습니다. 감속기는 동작 및 상태를 수신합니다. 작업 유형을 이해하고 데이터를 처리하며 새로운 상태를 반환합니다. 이제 구성 요소에서 새 상태를 사용할 수 있습니다.


연습으로 이 후크에 대해 더 잘 이해하기 위해 useState를 대신 사용하려고 했습니다.


useState는 PokemonProvider 내부에 있었습니다. 이 데이터 구조에서 사용 가능하고 캡처 된 포켓몬의 초기 상태를 재정의 할 수 있습니다.


const defaultState = {
  pokemons: [
    { id: 1, name: 'Bulbasaur' },
    { id: 2, name: 'Charmander' },
    { id: 3, name: 'Squirtle' }
  ],
  capturedPokemons: []
};

이 값을 useReducer에 전달하십시오.


const [state, dispatch] = useReducer(pokemonReducer, defaultState);

useReducer는 감속기와 초기 상태의 두 매개 변수를 수신합니다. 지금 pokemonReducer를 만들어 봅시다.


감속기는 현재 상태와 디스패치 된 조치를 수신합니다.


const pokemonReducer = (state, action) => // returns the new state based on the action type

여기서 우리는 액션 타입을 얻고 새로운 상태를 반환합니다. 동작은 객체입니다. 다음과 같이 보입니다 :


{ type: 'AN_ACTION_TYPE' }

그러나 더 클 수도 있습니다.


{
  type: 'AN_ACTION_TYPE',
  pokemon: {
    name: 'Pikachu'
  }
}

이 경우 포켓몬을 액션 객체에 전달합니다. 잠깐 멈추고 감속기 안에서 하고 싶은 일을 생각해 봅시다.


여기서는 일반적으로 데이터를 업데이트하고 작업을 처리합니다. 액션이 전달되므로 액션이 동작입니다. 그리고 우리 앱의 행동은 캡처 및 릴리스입니다! 여기에서 처리해야 할 작업이 있습니다.



이것이 감속기의 모습입니다 :


const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      // handle capture and return new state
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

액션 유형이 CAPTURE 인 경우 한 가지 방식으로 처리합니다. 액션 유형이 RELEASE 인 경우 다른 방식으로 처리합니다. 액션 타입이 이 타입들 중 어느 것과도 일치하지 않는다면, 현재 상태를 반환하십시오.


포켓몬을 캡처 할 때 두 목록을 모두 업데이트해야 합니다. 사용 가능한 목록에서 포켓몬을 제거하고 캡처 된 목록에 추가하십시오. 이 상태는 감속기에서 돌아와야 합니다.


const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

capturePokemon 함수는 업데이트 된 목록 만 반환합니다. getPokemonsList는 사용 가능한 목록에서 캡처 된 포켓몬을 제거합니다.


그리고 우리는 감속기 에서 이 새로운 기능을 사용합니다 :


const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      // handle release and return new state
    default:
      return state;
  }
};

이제 릴리스 함수!


const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

getCapturedPokemons는 캡처 된 목록에서 릴리스 된 포켓몬을 제거합니다. releasePokemon 함수는 업데이트 된 목록을 반환합니다.


감속기는 이제 다음과 같습니다.


const pokemonReducer = (state, action) => {
  switch (action.type) {
    case 'CAPTURE':
      return capturePokemon(action.pokemon, state);
    case 'RELEASE':
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

단 하나의 사소한 리 팩터 : 액션 타입! 이들은 문자열이며 상수로 추출하여 디스패처를 제공 할 수 있습니다.


export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

그리고 reducer :


const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

전체 리듀서 파일은 다음과 같습니다.


export const CAPTURE = 'CAPTURE';
export const RELEASE = 'RELEASE';

const getCapturedPokemons = (capturedPokemons, releasedPokemon) =>
  capturedPokemons.filter(pokemon => pokemon !== releasedPokemon)

const releasePokemon = (releasedPokemon, state) => ({
  pokemons: [...state.pokemons, releasedPokemon],
  capturedPokemons: getCapturedPokemons(state.capturedPokemons, releasedPokemon)
});

const getPokemonsList = (pokemons, capturedPokemon) =>
  pokemons.filter(pokemon => pokemon !== capturedPokemon)

const capturePokemon = (pokemon, state) => ({
  pokemons: getPokemonsList(state.pokemons, pokemon),
  capturedPokemons: [...state.capturedPokemons, pokemon]
});

export const pokemonReducer = (state, action) => {
  switch (action.type) {
    case CAPTURE:
      return capturePokemon(action.pokemon, state);
    case RELEASE:
      return releasePokemon(action.pokemon, state);
    default:
      return state;
  }
};

리듀서가 구현되었으므로 이를 제공자로 가져 와서 useReducer 후크에서 사용할 수 있습니다.


const [state, dispatch] = useReducer(pokemonReducer, defaultState);

우리는 PokemonProvider 내부에 있으므로 소비 구성 요소에 몇 가지 가치를 제공하고자 합니다 : 캡처 및 해제 조치.


이 함수는 올바른 액션 유형을 전달하고 포켓몬을 reducer로 전달하면 됩니다.


  • 캡처 기능 : 포켓몬을 수신하고 CAPTURE 유형 및 캡처 된 포켓몬을 사용하여 작업을 전달하는 새 함수를 반환합니다.
const capture = (pokemon) => () => {
  dispatch({ type: CAPTURE, pokemon });
};
  • 릴리스 함수 : 포켓몬을 수신하고 RELEASE 유형 및 릴리스 된 포켓몬으로 조치를 디스패치하는 새 함수를 리턴합니다.
const release = (pokemon) => () => {
  dispatch({ type: RELEASE, pokemon });
};

이제 상태와 동작이 구현되면 소비하는 구성 요소에 이러한 값을 제공 할 수 있습니다. 공급자 가치 제안을 업데이트하십시오.


const { pokemons, capturedPokemons } = state;

const providerValue = {
  pokemons,
  capturedPokemons,
  release,
  capture
};

<PokemonContext.Provider value={providerValue}>
  {props.children}
</PokemonContext.Provider>

이제 컴포넌트로 돌아갑니다. 이 새로운 행동들을 사용합시다. 모든 캡처 및 릴리스 논리는 공급자 및 감속기에 캡슐화되어 있습니다. 우리의 구성 요소는 이제 꽤 깨끗합니다 useContext는 다음과 같습니다.


const { pokemons, capture } = useContext(PokemonContext);

그리고 전체 구성 요소 :


import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const PokemonsList = () => {
  const { pokemons, capture } = useContext(PokemonContext);

  return (
    <div className="pokemons-list">
      <h2>Pokemons List</h2>
      
      {pokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <span>{pokemon.name}</span>
          <button onClick={capture(pokemon)}>+</button>
        </div>)}
    </div>
  )
};

export default PokemonsList;

캡처 한 포켓몬 구성 요소의 경우 useContext와 매우 유사하게 보입니다.


const { capturedPokemons, release } = useContext(PokemonContext);

그리고 전체 구성 요소 :


import React, { useContext } from 'react';
import { PokemonContext } from './PokemonContext';

const Pokedex = () => {
  const { capturedPokemons, release } = useContext(PokemonContext);

  return (
    <div className="pokedex">
      <h2>Pokedex</h2>

      {capturedPokemons.map((pokemon) =>
        <div key={`${pokemon.id}-${pokemon.name}`}>
          <span>{pokemon.name}</span>
          <button onClick={release(pokemon)}>-</button>
        </div>)}
    </div>
  )
};

export default Pokedex;

논리가 없습니다. UI 만 매우 깨끗합니다.