웹사이트 검색

React Hooks 및 Context API로 CRUD 앱을 빌드하는 방법


소개

이 기사에서는 React 후크(버전 16.8에 도입됨)를 다룰 것입니다.

Context API의 도입으로 한 가지 주요 문제인 소품 드릴링이 해결되었습니다. 중첩된 심층 구성 요소 계층을 통해 한 구성 요소에서 다른 구성 요소로 데이터를 가져오는 프로세스입니다. React 후크를 사용하면 클래스 기반 구성 요소가 아닌 기능적 구성 요소를 사용할 수 있습니다. 수명 주기 방법을 활용해야 하는 경우에는 클래스 기반 접근 방식을 사용해야 했습니다. 이제 더 이상 super(props)를 호출하거나 바인딩 메서드 또는 this 키워드에 대해 걱정할 필요가 없습니다.

이 기사에서는 Context API와 React 후크를 함께 사용하여 직원 목록을 에뮬레이트하는 완전한 기능의 CRUD 애플리케이션을 빌드합니다. 직원 데이터를 읽고, 새 직원을 만들고, 직원 데이터를 업데이트하고, 직원을 삭제합니다. 이 자습서에서는 외부 API 호출을 사용하지 않습니다. 데모를 위해 상태로 사용할 하드 코딩된 개체를 사용합니다.

전제 조건

이 자습서를 완료하려면 다음이 필요합니다.

  • Node.js용 로컬 개발 환경입니다. Node.js 설치 및 로컬 개발 환경 생성 방법을 따르십시오.
  • React 구성 요소 가져오기, 내보내기 및 렌더링에 대한 이해. React.js에서 코딩하는 방법 시리즈를 살펴볼 수 있습니다.

이 튜토리얼은 Node v15.3.0, npm v7.4.0, react v17.0.1, react-router-dom v5.2.0, tailwindcss-cli v0.1.2 및 tailwindcss v2.0.2.

1단계 - 프로젝트 설정

먼저 다음 명령과 함께 Create React App을 사용하여 React 프로젝트를 설정하는 것으로 시작합니다.

  1. npx create-react-app react-crud-employees-example

새로 생성된 프로젝트 디렉토리로 이동합니다.

  1. cd react-crud-employees-example

다음으로 다음 명령을 실행하여 react-router-dom을 종속 항목으로 추가합니다.

  1. npm install react-router-dom@5.2.0

참고: React Router에 대한 추가 정보는 React Router 튜토리얼을 참조하십시오.

그런 다음 src 디렉토리로 이동합니다.

cd src

다음 명령을 사용하여 Tailwind CSS의 기본 빌드를 프로젝트에 추가합니다.

  1. npx tailwindcss-cli@0.1.2 build --output tailwind.css

참고: Tailwind CSS에 대한 자세한 내용은 Tailwind CSS 자습서를 참조하세요.

그런 다음 코드 편집기에서 index.js를 열고 tailwind.cssBrowserRouter를 사용하도록 수정합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './tailwind.css';
import './index.css';
import App from './App';

ReactDOM.render(
  <BrowserRouter>
    <App />
  <BrowserRouter>
  document.getElementById('root')
);

이 시점에서 Tailwind CSSreact-router-dom이 포함된 새로운 React 프로젝트가 생성됩니다.

2단계 — AppReducer 및 GlobalContext 빌드

먼저 src 디렉토리 아래에 새 context 디렉토리를 만듭니다.

이 새 디렉터리에서 새 AppReducer.js 파일을 만듭니다. 이 감속기는 ADD_EMPLOYEE, EDIT_EMPLOYEEREMOVE_EMPLOYEE와 같은 CRUD 작업을 정의합니다. 코드 편집기에서 이 파일을 열고 다음 코드 줄을 추가합니다.

export default function appReducer(state, action) {
  switch (action.type) {
    case "ADD_EMPLOYEE":
      return {
        ...state,
        employees: [...state.employees, action.payload],
      };

    case "EDIT_EMPLOYEE":
      const updatedEmployee = action.payload;

      const updatedEmployees = state.employees.map((employee) => {
        if (employee.id === updatedEmployee.id) {
          return updatedEmployee;
        }
        return employee;
      });

      return {
        ...state,
        employees: updatedEmployees,
      };

    case "REMOVE_EMPLOYEE":
      return {
        ...state,
        employees: state.employees.filter(
          (employee) => employee.id !== action.payload
        ),
      };

    default:
      return state;
  }
};

ADD_EMPLOYEES는 새 직원이 포함된 페이로드 값을 가져와 업데이트된 직원 상태를 반환합니다.

EDIT_EMPLOYEE는 페이로드 값을 가져와 id를 직원과 비교합니다. 일치하는 항목이 있으면 새 페이로드 값을 사용하고 업데이트된 직원 상태를 반환합니다.

REMOVE_EMPLOYEE는 페이로드 값을 가져와 id를 직원과 비교합니다. 일치하는 항목이 있으면 해당 직원을 제거하고 업데이트된 직원 상태를 반환합니다.

context 디렉터리에 남아 있는 동안 새 GlobalState.js 파일을 만듭니다. 요청에서 반환된 직원 데이터를 에뮬레이트하기 위해 초기 하드 코딩된 값이 포함됩니다. 코드 편집기에서 이 파일을 열고 다음 코드 줄을 추가합니다.

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

import appReducer from './AppReducer';

const initialState = {
  employees: [
    {
      id: 1,
      name: "Sammy",
      location: "DigitalOcean",
      designation: "Shark"
    }
  ]
};

export const GlobalContext = createContext(initialState);

export const GlobalProvider = ({ children }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  function addEmployee(employee) {
    dispatch({
      type: "ADD_EMPLOYEE",
      payload: employee
    });
  }

  function editEmployee(employee) {
    dispatch({
      type: "EDIT_EMPLOYEE",
      payload: employee
    });
  }

  function removeEmployee(id) {
    dispatch({
      type: "REMOVE_EMPLOYEE",
      payload: id
    });
  }

  return (
    <GlobalContext.Provider
      value={{
        employees: state.employees,
        addEmployee,
        editEmployee,
        removeEmployee
      }}
    >
      {children}
    </GlobalContext.Provider>
  );
};

이 코드는 각 작업에 해당하는 사례를 전환하기 위해 리듀서 파일로 이동하는 작업을 디스패치하는 몇 가지 기능을 추가합니다.

이 시점에서 AppReducer.jsGlobalState.js가 포함된 React 애플리케이션이 있어야 합니다.

애플리케이션이 제대로 작동하는지 확인하기 위해 EmployeeList 구성 요소를 생성해 보겠습니다. src 디렉토리로 이동하여 새 components 디렉토리를 만듭니다. 해당 디렉터리에서 새 EmployeeList.js 파일을 만들고 다음 코드를 추가합니다.

import React, { useContext } from 'react';

import { GlobalContext } from '../context/GlobalState';

export const EmployeeList = () => {
  const { employees } = useContext(GlobalContext);
  return (
    <React.Fragment>
      {employees.length > 0 ? (
        <React.Fragment>
          {employees.map((employee) => (
            <div
              className="flex items-center bg-gray-100 mb-10 shadow"
              key={employee.id}
            >
              <div className="flex-auto text-left px-4 py-2 m-2">
                <p className="text-gray-900 leading-none">
                  {employee.name}
                </p>
                <p className="text-gray-600">
                  {employee.designation}
                </p>
                <span className="inline-block text-sm font-semibold mt-1">
                  {employee.location}
                </span>
              </div>
            </div>
          ))}
        </React.Fragment>
      ) : (
        <p className="text-center bg-gray-100 text-gray-500 py-5">No data.</p>
      )}
    </React.Fragment>
  );
};

이 코드는 모든 직원employee.name, employee.designationemployee.location을 표시합니다.

그런 다음 코드 편집기에서 App.js를 엽니다. 그리고 EmployeeListGlobalProvider를 추가합니다.

import { EmployeeList } from './components/EmployeeList';

import { GlobalProvider } from './context/GlobalState';

function App() {
  return (
    <GlobalProvider>
      <div className="App">
        <EmployeeList />
      </div>
    </GlobalProvider>
  );
}

export default App;

애플리케이션을 실행하고 웹 브라우저에서 관찰합니다.

EmployeeList 구성 요소는 GlobalState.js에 설정된 하드 코딩된 값을 표시합니다.

3단계 - AddEmployee 및 EditEmployee 구성 요소 구축

이 단계에서는 새 직원 만들기 및 기존 직원 업데이트를 지원하는 구성 요소를 빌드합니다.

이제 components 디렉토리로 다시 이동합니다. 새 AddEmployee.js 파일을 만듭니다. 이는 양식 필드의 값을 상태로 푸시하는 onSubmit 핸들러를 포함하는 AddEmployee 구성 요소 역할을 합니다.

import React, { useState, useContext } from 'react';
import { Link, useHistory } from 'react-router-dom';

import { GlobalContext } from '../context/GlobalState';

export const AddEmployee = () => {
  let history = useHistory();

  const { addEmployee, employees } = useContext(GlobalContext);

  const [name, setName] = useState("");
  const [location, setLocation] = useState("");
  const [designation, setDesignation] = useState("");

  const onSubmit = (e) => {
    e.preventDefault();
    const newEmployee = {
      id: employees.length + 1,
      name,
      location,
      designation,
    };
    addEmployee(newEmployee);
    history.push("/");
  };

  return (
    <React.Fragment>
      <div className="w-full max-w-sm container mt-20 mx-auto">
        <form onSubmit={onSubmit}>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="name"
            >
              Name of employee
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
              value={name}
              onChange={(e) => setName(e.target.value)}
              type="text"
              placeholder="Enter name"
            />
          </div>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="location"
            >
              Location
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={location}
              onChange={(e) => setLocation(e.target.value)}
              type="text"
              placeholder="Enter location"
            />
          </div>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="designation"
            >
              Designation
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
              value={designation}
              onChange={(e) => setDesignation(e.target.value)}
              type="text"
              placeholder="Enter designation"
            />
          </div>
          <div className="flex items-center justify-between">
            <button className="mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
              Add Employee
            </button>
          </div>
          <div className="text-center mt-4 text-gray-500">
            <Link to="/">Cancel</Link>
          </div>
        </form>
      </div>
    </React.Fragment>
  );
};

이 코드에서 setName, setLocationsetDesignation은 사용자가 양식 필드에 입력하는 현재 값을 사용합니다. 이러한 값은 고유한 id(총 길이에 1을 추가함)를 사용하여 새 상수인 newEmployee로 래핑됩니다. 그러면 새로 추가된 직원을 포함하여 업데이트된 직원 목록이 표시되는 메인 화면으로 경로가 변경됩니다.

AddEmployee 구성 요소는 내장된 React Hooks 중 하나인 GlobalStateuseContext를 가져와 기능 구성 요소가 컨텍스트에 쉽게 액세스할 수 있도록 합니다.

employees 개체, removeEmployeeeditEmployeesGlobalState.js 파일에서 가져왔습니다.

여전히 components 디렉토리에 있는 동안 새 EditEmployee.js 파일을 만듭니다. 이것은 상태에서 기존 개체를 편집하는 기능을 포함하는 editEmployee 구성 요소 역할을 합니다.

import React, { useState, useContext, useEffect } from 'react';
import { useHistory, Link } from 'react-router-dom';

import { GlobalContext } from '../context/GlobalState';

export const EditEmployee = (route) => {
  let history = useHistory();

  const { employees, editEmployee } = useContext(GlobalContext);

  const [selectedUser, setSelectedUser] = useState({
    id: null,
    name: "",
    designation: "",
    location: "",
  });

  const currentUserId = route.match.params.id;

  useEffect(() => {
    const employeeId = currentUserId;
    const selectedUser = employees.find(
      (currentEmployeeTraversal) => currentEmployeeTraversal.id === parseInt(employeeId)
    );
    setSelectedUser(selectedUser);
  }, [currentUserId, employees]);

  const onSubmit = (e) => {
    e.preventDefault();
    editEmployee(selectedUser);
    history.push("/");
  };

  const handleOnChange = (userKey, newValue) =>
    setSelectedUser({ ...selectedUser, [userKey]: newValue });

  if (!selectedUser || !selectedUser.id) {
    return <div>Invalid Employee ID.</div>;
  }

  return (
    <React.Fragment>
      <div className="w-full max-w-sm container mt-20 mx-auto">
        <form onSubmit={onSubmit}>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="name"
            >
              Name of employee
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.name}
              onChange={(e) => handleOnChange("name", e.target.value)}
              type="text"
              placeholder="Enter name"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="location"
            >
              Location
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.location}
              onChange={(e) => handleOnChange("location", e.target.value)}
              type="text"
              placeholder="Enter location"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="designation"
            >
              Designation
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.designation}
              onChange={(e) => handleOnChange("designation", e.target.value)}
              type="text"
              placeholder="Enter designation"
            />
          </div>
          <div className="flex items-center justify-between">
            <button className="block mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:text-gray-600 focus:shadow-outline">
              Edit Employee
            </button>
          </div>
          <div className="text-center mt-4 text-gray-500">
            <Link to="/">Cancel</Link>
          </div>
        </form>
      </div>
    </React.Fragment>
  );
};

이 코드는 구성 요소가 마운트될 때 호출되는 useEffect 후크를 사용합니다. 이 후크 내에서 현재 경로 매개변수는 상태의 employees 개체에 있는 동일한 매개변수와 비교됩니다.

onChange 이벤트 리스너는 사용자가 양식 필드를 변경할 때 트리거됩니다. userKeynewValuesetSelectedUser로 전달됩니다. selectedUser가 확산되고 userKey가 키로 설정되고 newValue가 값으로 설정됩니다.

4단계 - 경로 설정

이 단계에서는 EmployeeList를 업데이트하여 AddEmployeeEditEmployee 구성 요소에 연결합니다.

EmployeeList.js를 다시 방문하여 LinkremoveEmployee를 사용하도록 수정합니다.

import React, { useContext } from 'react';
import { Link } from 'react-router-dom';

import { GlobalContext } from '../context/GlobalState';

export const EmployeeList = () => {
  const { employees, removeEmployee } = useContext(GlobalContext);
  return (
    <React.Fragment>
      {employees.length > 0 ? (
        <React.Fragment>
          {employees.map((employee) => (
            <div
              className="flex items-center bg-gray-100 mb-10 shadow"
              key={employee.id}
            >
              <div className="flex-auto text-left px-4 py-2 m-2">
                <p className="text-gray-900 leading-none">
                  {employee.name}
                </p>
                <p className="text-gray-600">
                  {employee.designation}
                </p>
                <span className="inline-block text-sm font-semibold mt-1">
                  {employee.location}
                </span>
              </div>
              <div className="flex-auto text-right px-4 py-2 m-2">
                <Link
                  to={`/edit/${employee.id}`}
                  title="Edit Employee"
                >
                  <div className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold mr-3 py-2 px-4 rounded-full inline-flex items-center">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
                  </div>
                </Link>
                <button
                  onClick={() => removeEmployee(employee.id)}
                  className="block bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-2 px-4 rounded-full inline-flex items-center"
                  title="Remove Employee"
                >
                  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
                </button>
              </div>
            </div>
          ))}
        </React.Fragment>
      ) : (
        <p className="text-center bg-gray-100 text-gray-500 py-5">No data.</p>
      )}
    </React.Fragment>
  );
};

이 코드는 직원 정보 옆에 두 개의 아이콘을 추가합니다. 연필과 종이 아이콘은 "편집\을 나타내고 EditEmployee 구성 요소에 대한 링크를 나타냅니다. 휴지통 아이콘은 "제거\를 나타내며 이 아이콘을 클릭하면 removeEmployee가 실행됩니다.

다음으로 HeadingHome이라는 두 개의 새 구성 요소를 만들어 EmployeeList 구성 요소를 표시하고 사용자에게 AddEmployee에 대한 액세스 권한을 제공합니다. 구성 요소.

components 디렉토리에서 새 Heading.js 파일을 만듭니다.

import React from "react";
import { Link } from "react-router-dom";

export const Heading = () => {
  return (
    <div>
      <div className="flex items-center mt-24 mb-10">
        <div className="flex-grow text-left px-4 py-2 m-2">
          <h5 className="text-gray-900 font-bold text-xl">Employee Listing</h5>
        </div>
        <div className="flex-grow text-right px-4 py-2 m-2">
          <Link to="/add">
            <button className="bg-green-400 hover:bg-green-500 text-white font-semibold py-2 px-4 rounded inline-flex items-center">
              <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
              <span className="pl-2">Add Employee</span>
            </button>
          </Link>
        </div>
      </div>
    </div>
  );
};

components 디렉터리에서 새 Home.js 파일을 만듭니다.

import React from "react";
import { Heading } from "./Heading";
import { EmployeeList } from "./EmployeeList";

export const Home = () => {
  return (
    <React.Fragment>
      <div className="container mx-auto">
        <h3 className="text-center text-3xl mt-20 text-base leading-8 text-black font-bold tracking-wide uppercase">
          CRUD with React Context API and Hooks
        </h3>
        <Heading />
        <EmployeeList />
      </div>
    </React.Fragment>
  );
};

App.js를 다시 방문하여 react-router-dom에서 RouteSwitch를 가져옵니다. Home, AddeEmployeeEditEmployee 구성 요소를 각 경로에 할당합니다.

import { Route, Switch } from 'react-router-dom';

import { GlobalProvider } from './context/GlobalState';

import { Home } from './components/Home';
import { AddEmployee } from './components/AddEmployee';
import { EditEmployee } from './components/EditEmployee';

function App() {
  return (
    <GlobalProvider>
      <div className="App">
        <Switch>
          <Route path="/" component={Home} exact />
          <Route path="/add" component={AddEmployee} exact />
          <Route path="/edit/:id" component={EditEmployee} exact />
        </Switch>
      </div>
    </GlobalProvider>
  );
}

export default App;

앱을 컴파일하고 브라우저에서 관찰합니다.

HeadingEmployeeList 구성 요소가 있는 Home 구성 요소로 라우팅됩니다.

직원 추가 링크를 클릭합니다. AddEmployee 구성 요소로 라우팅됩니다.

신입 사원에 대한 정보를 제출하면 구성 요소로 다시 라우팅되며 이제 신입 사원이 나열됩니다.

직원 편집 링크를 클릭합니다. EditEmployee 구성 요소로 라우팅됩니다.

직원에 대한 정보를 수정한 후 구성 요소로 다시 라우팅되며 이제 업데이트된 세부 정보와 함께 새 직원이 나열됩니다.

결론

이 기사에서는 Context API와 React 후크를 함께 사용하여 완전히 작동하는 CRUD 애플리케이션을 빌드했습니다.

React에 대해 자세히 알아보려면 연습 및 프로그래밍 프로젝트에 대한 React 주제 페이지를 살펴보세요.