분류 Reactjs

React 및 FeathersJS를 사용하여 Node.js CRUD 앱 빌드

컨텐츠 정보

  • 조회 37 (작성일 )

본문

최신 프로젝트를 구축하려면 로직을 프런트 엔드 및 백 엔드 코드로 분할해야 합니다. 이러한 움직임의 이유는 코드 재사용을 촉진하기 위한 것입니다. 예를 들어 백엔드 API에 액세스하는 기본 모바일 애플리케이션을 빌드 해야 할 수 있습니다. 또는 대규모 모듈식 플랫폼의 일부가 될 모듈을 개발할 수 있습니다.


https://www.sitepoint.com/crud-app-node-react-feathersjs/


서버 측 API를 구축하는 인기 있는 방법은 Express 또는 Restify와 같은 라이브러리와 함께 Node.js를 사용하는 것입니다. 이러한 라이브러리를 사용하면 RESTful 경로를 쉽게 만들 수 있습니다. 

이러한 라이브러리의 문제는 우리가 수많은 반복 코드를 작성한다는 것입니다. 또한 권한 부여 및 기타 미들웨어 논리를 위한 코드를 작성해야 합니다.


이 딜레마를 피하기 위해 Feathers와 같은 프레임 워크를 사용하여 몇 가지 명령만으로 API를 생성 할 수 있습니다.


Feathers를 놀랍게 만드는 것은 단순성입니다. 전체 프레임 워크는 모듈식이며 필요한 기능만 설치하면 됩니다. Feathers 자체는 Express 위에 구축 된 얇은 래퍼로 서비스후크와 같은 새로운 기능을 추가했습니다. Feathers를 사용하면 WebSocket을 통해 데이터를 쉽게 보내고 받을 수 있습니다.


전제 조건 


이 자습서를 진행하려면 컴퓨터에 다음 항목이 설치되어 있어야 합니다.


  • Node.js v12 + 및 최신 버전의 npm. 설정하는 데 도움이 필요한 경우 이 자습서를 확인하십시오.
  • MongoDB v4.2 이상. 설정하는 데 도움이 필요한 경우 이 자습서를 확인하십시오.
  • Yarn 패키지 관리자 — npm i -g yarn을 사용하여 설치됩니다.

다음 주제에 익숙한 경우에도 도움이 됩니다.


또한 완성 된 프로젝트 코드는 GitHub에서 찾을 수 있습니다.


Scaffold the App 


Node.js, React, Feathers 및 MongoDB를 사용하여 CRUD 연락처 관리자 애플리케이션을 구축 할 것입니다.


이 튜토리얼에서는 아래에서 위로 애플리케이션을 빌드하는 방법을 보여 드리겠습니다. 인기 있는 Create React App 도구를 사용하여 프로젝트를 시작합니다.


다음과 같이 설치할 수 있습니다.


npm install -g create-react-app

그런 다음 새 프로젝트를 만듭니다.


# scaffold a new react project
create-react-app react-contact-manager
cd react-contact-manager

# delete unnecessary files
rm src/logo.svg src/App.css src/serviceWorker.js

좋아하는 코드 편집기를 사용하고 src / index.css의 모든 콘텐츠를 제거합니다. 그런 다음 src / App.js를 열고 다음과 같이 코드를 다시 작성합니다.


import React from 'react';

const App = () => {
  return (
    <div>
      <h1>Contact Manager</h1>
    </div>
  );
};

export default App;

그리고 src / index.js에서 다음과 같이 코드를 변경합니다.


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

react-contact-manager 디렉터리에서 yarn start를 실행하여 프로젝트를 시작합니다. 브라우저가 자동으로 http : // localhost : 3000을 열고 "연락처 관리자"라는 제목을 볼 수 있습니다. 콘솔 탭을 빠르게 확인하여 프로젝트가 경고 나 오류 없이 깨끗하게 실행되고 있는지 확인하고 모든 것이 원활하게 실행되고 있으면 Ctrl + C를 사용하여 서버를 중지합니다.


Feathers로 API 서버 구축 


feathers-cli 도구를 사용하여 CRUD 프로젝트용 백엔드 API를 생성 해 보겠습니다.


# Install Feathers command-line tool
npm install @feathersjs/cli -g

# Create directory for the back-end code
# Run this command in the `react-contact-manager` directory
mkdir backend
cd backend

# Generate a feathers back-end API server
feathers generate app

? Do you want to use JavaScript or TypeScript? JavaScript
? Project name backend
? Description Contacts API server
? What folder should the source files live in? src
? Which package manager are you using (has to be installed globally)? Yarn
? What type of API are you making? REST, Realtime via Socket.io
? Which testing framework do you prefer? Mocha + assert
? This app uses authentication No
? Which coding style do you want to use? ESLint

# Ensure Mongodb is running
sudo service mongod start
sudo service mongod status

● mongod.service - MongoDB Database Server
   Loaded: loaded (/lib/systemd/system/mongod.service; disabled; vendor preset: enabled)
   Active: active (running) since Fri 2020-09-18 14:42:12 CEST; 4s ago
     Docs: https://docs.mongodb.org/manual
 Main PID: 31043 (mongod)
   CGroup: /system.slice/mongod.service
           └─31043 /usr/bin/mongod --config /etc/mongod.conf

# Generate RESTful routes for Contact Model
feathers generate service

? What kind of service is it? Mongoose
? What is the name of the service? contacts
? Which path should the service be registered on? /contacts
? What is the database connection string? mongodb://localhost:27017/contactsdb

# Install email and unique field validation
yarn add mongoose-type-email

backend / config / default.json을 열어 보겠습니다. 여기에서 MongoDB 연결 매개 변수 및 기타 설정을 구성 할 수 있습니다. 이 자습서에서는 프런트 엔드 페이지 매김이 다루지 않으므로 기본 페이지 매기기 값을 50으로 변경합니다.


{
  "host": "localhost",
  "port": 3030,
  "public": "../public/",
  "paginate": {
    "default": 50,
    "max": 50
  },
  "mongodb": "mongodb://localhost:27017/contactsdb"
}

backend / src / models / contact.model.js를 열고 다음과 같이 코드를 업데이트합니다.


require('mongoose-type-email');

module.exports = function (app) {
  const modelName = 'contacts';
  const mongooseClient = app.get('mongooseClient');
  const { Schema } = mongooseClient;
  const schema = new Schema({
    name : {
      first: {
        type: String,
        required: [true, 'First Name is required']
      },
      last: {
        type: String,
        required: false
      }
    },
    email : {
      type: mongooseClient.SchemaTypes.Email,
      required: [true, 'Email is required']
    },
    phone : {
      type: String,
      required: [true, 'Phone is required'],
      validate: {
        validator: function(v) {
          return /^\+(?:[0-9] ?){6,14}[0-9]$/.test(v);
        },
        message: '{VALUE} is not a valid international phone number!'
      }
    }
  }, {
    timestamps: true
  });

  // This is necessary to avoid model compilation errors in watch mode
  // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel
  if (mongooseClient.modelNames().includes(modelName)) {
    mongooseClient.deleteModel(modelName);
  }

  return mongooseClient.model(modelName, schema);
};

Mongoose는 두 개의 새 필드 인 createdAt 및 updatedAt을 삽입하는 타임 스탬프라는 새로운 기능을 도입했습니다. 이 두 필드는 레코드를 생성하거나 업데이트 할 때마다 자동으로 채워집니다.

또한 mongoose-type-email 플러그인을 설치하여 서버에서 이메일 유효성 검사를 수행했습니다.


이제 backend / src / mongoose.js를 열고 다음 줄을 변경합니다.


{ useCreateIndex: true, useNewUrlParser: true }

to:


{
  useCreateIndex: true,
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
}

이로 인해 몇 가지 성가신 지원 중단 경고가 표시됩니다.


새 터미널을 열고 백엔드 디렉토리 내에서 yarn 테스트를 실행합니다. 모든 테스트가 성공적으로 실행되어야 합니다.

그런 다음 yarn start를 실행하여 백엔드 서버를 시작하십시오. 서버가 초기화되면 'Localhost : 3030에서 시작된 페더스 애플리케이션'을 콘솔에 출력해야 합니다.


브라우저를 시작하고 URL http : // localhost : 3030 / contacts에 액세스합니다. 다음 JSON 응답을 받아야 합니다.


{"total":0,"limit":50,"skip":0,"data":[]}

Hoppscotch로 API 테스트 


이제 Hoppscotch (이전 Postwoman)를 사용하여 모든 엔드 포인트가 제대로 작동하는지 확인하겠습니다.


먼저 연락처를 만들어 보겠습니다. 이 링크는 / contacts 엔드 포인트에 POST 요청을 보내도록 설정된 모든 항목과 함께 Hoppscotch를 엽니다. Raw 입력이 켜져 있는지 확인한 다음 녹색 보내기 버튼을 눌러 새 연락처를 만듭니다.

응답은 다음과 같아야 합니다.


{
  "_id": "5f64832c20745f4f282b39f9",
  "name": {
    "first": "Tony",
    "last": "Stark"
  },
  "phone": "+18138683770",
  "email": "tony@starkenterprises.com",
  "createdAt": "2020-09-18T09:51:40.021Z",
  "updatedAt": "2020-09-18T09:51:40.021Z",
  "__v": 0
}

이제 새로 생성 된 연락처를 검색해 보겠습니다. 이 링크는 / contacts 엔드 포인트에 GET 요청을 보낼 준비가 된 Hoppscotch를 엽니다. 보내기 버튼을 누르면 다음과 같은 응답을 받아야 합니다.


{
  "total": 1,
  "limit": 50,
  "skip": 0,
  "data": [
    {
      "_id": "5f64832c20745f4f282b39f9",
      "name": {
        "first": "Tony",
        "last": "Stark"
      },
      "phone": "+18138683770",
      "email": "tony@starkenterprises.com",
      "createdAt": "2020-09-18T09:51:40.021Z",
      "updatedAt": "2020-09-18T09:51:40.021Z",
      "__v": 0
    }
  ]
}

http : // localhost : 3030 / contacts / <_id>로 GET 요청을 보내 Hoppscotch에서 개별 연락처를 표시 할 수 있습니다. _id 필드는 항상 고유하므로 이전 단계에서 받은 응답에서 복사해야 합니다. 위의 예에 대한 링크입니다. 보내기를 누르면 연락처가 표시됩니다.


http : // localhost : 3030 / contacts / <_id>에 PUT 요청을 보내고 업데이트 된 데이터를 JSON으로 전달하여 연락처를 업데이트 할 수 있습니다. 위의 예에 대한 링크입니다. 보내기를 누르면 연락처가 업데이트 됩니다.


마지막으로 동일한 주소 (즉, http : // localhost : 3030 / contacts / <_id>)로 DELETE 요청을 보내 연락처를 제거 할 수 있습니다. 위의 예에 대한 링크입니다. 보내기를 누르면 연락처가 삭제됩니다.


Hoppscotch는 매우 다재다능한 도구이며 다음 단계로 이동하기 전에 API가 예상대로 작동하는지 확인하기 위해 이 도구를 사용하는 것이 좋습니다.


사용자 인터페이스 구축 


원래 스타일링에 시맨틱 UI를 사용하고 싶었지만 글을 쓰는 시점에는 2 년 넘게 업데이트 되지 않았습니다. 다행스럽게도 오픈 소스 커뮤니티는 인기 있는 포크 인 Fomantic-UI를 만들어 프로젝트를 유지해 왔으며 이것이 우리가 사용할 것입니다. Semantic UI의 활발한 개발이 재개 될 때 하나를 다시 다른 하나로 병합 할 계획이 있습니다.


또한 Semantic UI React를 사용하여 많은 클래스 이름을 정의 할 필요 없이 사용자 인터페이스를 빠르게 구축 할 것입니다. 다행히도 이 프로젝트는 최신 상태로 유지되었습니다.


마지막으로 React Router를 사용하여 라우팅을 처리합니다.


이 과정을 마치고 react-contact-manager 디렉터리에서 새 터미널을 열고 다음 명령을 입력합니다.


# Install Fomantic UI CSS and Semantic UI React
yarn add fomantic-ui-css semantic-ui-react

# Install React Router
yarn add react-router-dom

다음 디렉토리와 파일을 src 디렉토리에 추가하여 프로젝트 구조를 업데이트하십시오.


src
├── App.js
├── App.test.js
├── components #(new)
│   ├── contact-form.js #(new)
│   └── contact-list.js #(new)
├── index.css
├── index.js
├── pages #(new)
│   ├── contact-form-page.js #(new)
│   └── contact-list-page.js #(new)
├── serviceWorker.js
└── setupTests.js

터미널에서 :


cd src
mkdir pages components
touch components/contact-form.js components/contact-list.js
touch pages/contact-form-page.js pages/contact-list-page.js

자리 표시 자 코드로 JavaScript 파일을 빠르게 채 웁니다.


ContactList 구성 요소는 기능 구성 요소 (React 요소를 반환하는 일반 JavaScript 함수)입니다.


// src/components/contact-list.js

import React from 'react';

const ContactList = () => {
  return (
    <div>
      <p>No contacts here</p>
    </div>
  );
}

export default ContactList;

최상위 컨테이너의 경우 페이지를 사용하고 있습니다. ContactListPage 구성 요소에 대한 몇 가지 코드를 제공하겠습니다.


// src/pages/contact-list-page.js

import React from 'react';
import ContactList from '../components/contact-list';

const ContactListPage = () => {
  return (
    <div>
      <h1>List of Contacts</h1>
      <ContactList />
    </div>
  );
};

export default ContactListPage;

ContactForm 구성 요소는 자체 상태, 특히 양식 필드를 관리해야 하므로 스마트해야 합니다. 이 작업은 React 후크로 수행 할 것입니다.


// src/components/contact-form.js

import React from 'react';

const ContactForm = () => {
  return (
    <div>
      <p>Form under construction</p>
    </div>
  )
}

export default ContactForm;

ContactFormPage 구성 요소를 다음 코드로 채웁니다.


// src/pages/contact-form-page.js

import React from 'react';
import ContactForm from '../components/contact-form';

const ContactFormPage = () => {
  return (
    <div>
      <ContactForm />
    </div>
  );
};

export default ContactFormPage;

이제 내비게이션 메뉴를 만들고 앱의 경로를 정의하겠습니다. App.js는 단일 페이지 응용 프로그램의 "레이아웃 템플릿"이라고도 합니다.


// src/App.js

import React from 'react';
import { NavLink, Route } from 'react-router-dom';
import { Container } from 'semantic-ui-react';
import ContactListPage from './pages/contact-list-page';
import ContactFormPage from './pages/contact-form-page';

const App = () => {
  return (
    <Container>
      <div className="ui two item menu">
        <NavLink className="item" activeClassName="active" exact to="/">
          Contacts List
        </NavLink>
        <NavLink
          className="item"
          activeClassName="active"
          exact
          to="/contacts/new"
        >
          Add Contact
        </NavLink>
      </div>
      <Route exact path="/" component={ContactListPage} />
      <Route path="/contacts/new" component={ContactFormPage} />
      <Route path="/contacts/edit/:_id" component={ContactFormPage} />
    </Container>
  );
};

export default App;

위 코드는 React Router를 사용합니다. 이에 대한 복습이 필요하면 자습서를 참조하십시오.


마지막으로 이 코드로 src / index.js 파일을 업데이트합니다. 여기서 스타일링을 위해 Formantic-UI를 가져오고 HTML5 히스토리 API를 사용하기 위해 BrowserRouter 구성 요소를 가져와 앱을 URL과 동기화 상태로 유지합니다.


// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'fomantic-ui-css/semantic.min.css';
import './index.css';

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

create-react-app 서버가 실행 중인지 확인한 다음 (아니라면 yarn start를 사용하여 시작) http : // localhost : 3000을 방문하십시오. 아래 스크린 샷과 유사한 보기가 있어야 합니다.


Screenshot of the empty list of contacts 

React Hooks 및 Context API로 상태 관리 


이전에는 React 앱에서 상태를 관리 할 때 Redux에 도달했을 수 있습니다. 하지만 React v16.8.0부터는 React Hooks와 Context API를 사용하여 React 애플리케이션에서 전역 상태를 관리 할 수 ​​있습니다.


이 새로운 기술을 사용하면 유지 관리하기 쉬운 코드를 적게 작성할 수 있습니다. 여전히 Redux 패턴을 사용하지만 React HooksContext API 만 사용합니다.


다음으로 Context API 연결을 살펴 보겠습니다.


컨텍스트 저장소 정의 


이것은 연락처에 대한 글로벌 상태를 처리하는 우리 매장과 같습니다. 상태는 연락처 배열, 로딩 상태, 백엔드 API 서버에서 생성 된 오류 메시지를 저장하기 위한 메시지 객체를 포함한 여러 변수로 구성됩니다.


src 디렉토리에서 contact-context.js 파일이 포함 된 컨텍스트 폴더를 만듭니다.


cd src
mkdir context
touch context/contact-context.js

그리고 다음 코드를 삽입하십시오.


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

export const ContactContext = createContext();

const initialState = {
  contacts: [],
  contact: {}, // selected or new
  message: {}, // { type: 'success|fail', title:'Info|Error' content:'lorem ipsum'}
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_CONTACTS': {
      return {
        ...state,
        contacts: action.payload,
      };
    }
    default:
      throw new Error();
  }
}

export const ContactContextProvider = props => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { children } = props;

  return (
    <ContactContext.Provider value={[state, dispatch]}>
      {children}
    </ContactContext.Provider>
  );
};

보시다시피 useState의 대안 인 useReducer 후크를 사용하고 있습니다. useReducer는 여러 하위 값을 포함하는 복잡한 상태 논리를 처리하는 데 적합합니다. 또한 다른 React 구성 요소와 데이터를 공유 할 수 있도록 Context API를 사용하고 있습니다.


컨텍스트 공급자를 응용 프로그램 루트에 삽입 


컨텍스트 공급자로 루트 구성 요소를 캡슐화해야 합니다. 다음과 같이 src / index.js를 업데이트합니다.


...
import { ContactContextProvider } from './context/contact-context';

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

이제 모든 하위 구성 요소는 useContext 후크를 사용하여 전역 상태에 액세스 할 수 있습니다.


연락처 목록 표시 


이 단계에서는 테스트 할 몇 가지 정적 데이터를 생성합니다. 초기 상태에는 빈 연락처 배열이 있습니다. dispatch 메서드를 사용하여 연락처 배열을 임시로 채 웁니다. pages / contact-list-page.js를 열고 다음과 같이 업데이트합니다.


import React, { useContext, useEffect } from 'react';
import ContactList from '../components/contact-list';
import { ContactContext } from '../context/contact-context';

const data = [
  {
    _id: '1',
    name: {
      first: 'John',
      last: 'Doe',
    },
    phone: '555',
    email: 'john@gmail.com',
  },
  {
    _id: '2',
    name: {
      first: 'Bruce',
      last: 'Wayne',
    },
    phone: '777',
    email: 'bruce.wayne@gmail.com',
  },
];

const ContactListPage = () => {
  const [state, dispatch] = useContext(ContactContext);

  useEffect(() => {
    dispatch({
      type: 'FETCH_CONTACTS',
      payload: data,
    });
  }, [dispatch]);

  return (
    <div>
      <h1>List of Contacts</h1>
      <ContactList contacts={state.contacts} />
    </div>
  );
};

export default ContactListPage;

다음으로, 간단한 루프를 사용하여 components / contact-list.js에 연락처를 표시합니다. 다음과 같이 업데이트하십시오.


import React from 'react';

const ContactList = ({ contacts }) => {
  const list = () => {
    return contacts.map(contact => {
      return (
        <li key={contact._id}>
          {contact.name.first} {contact.name.last}
        </li>
      );
    });
  };

  return (
    <div>
      <ul>{list()}</ul>
    </div>
  );
}

export default ContactList;

이제 브라우저로 돌아 가면 다음과 같이 표시됩니다.


Screenshot of the contact list showing two contacts 

Semantic UI 스타일링을 사용하여 목록 UI를 더 매력적으로 보이게 만들어 보겠습니다. src / components 폴더에서 새 파일 contact-card.js를 만듭니다.


touch src/components/contact-card.js

그런 다음 다음 코드를 추가하십시오.


// src/components/contact-card.js

import React from 'react';
import { Card, Button, Icon } from 'semantic-ui-react';

const ContactCard = ({ contact }) => {
  return (
    <Card>
      <Card.Content>
        <Card.Header>
          <Icon name="user outline" /> {contact.name.first} {contact.name.last}
        </Card.Header>
        <Card.Description>
          <p>
            <Icon name="phone" /> {contact.phone}
          </p>
          <p>
            <Icon name="mail outline" /> {contact.email}
          </p>
        </Card.Description>
      </Card.Content>
      <Card.Content extra>
        <div className="ui two buttons">
          <Button basic color="green">
            Edit
          </Button>
          <Button basic color="red">
            Delete
          </Button>
        </div>
      </Card.Content>
    </Card>
  );
}

export default ContactCard;

새 ContactCard 구성 요소를 사용하도록 ContactList 구성 요소를 업데이트하십시오.


// src/components/contact-list.js

import React from 'react';
import { Card } from 'semantic-ui-react';
import ContactCard from './contact-card';

const ContactList = ({ contacts }) => {
  const cards = () => {
    return contacts.map(contact => {
      return <ContactCard key={contact._id} contact={contact} />;
    });
  };

  return <Card.Group>{cards()}</Card.Group>;
}

export default ContactList;

이제 목록 페이지가 다음과 같이 표시됩니다.


The two contacts rendered with the semantic-ui styles 

Feathers API 서버에서 비동기식으로 데이터 가져 오기 


이제 전역 상태가 다른 React 구성 요소와 올바르게 공유되고 있음을 알았으므로 데이터베이스에 대한 실제 가져 오기 요청을 만들고 데이터를 사용하여 연락처 목록 페이지를 채울 수 있습니다. 이를 수행하는 방법에는 여러 가지가 있지만 제가 보여 드리는 방법은 놀랍도록 간단합니다.


먼저 Mongo 데이터베이스와 백엔드 서버가 모두 별도의 터미널에서 실행되고 있는지 확인합니다. URL http : // localhost : 3030 / contacts를 열어 확인할 수 있습니다. 결과가 반환 되지 않으면 페이지를 백업하고 Hoppscotch를 사용하여 연락처를 추가하세요.


다음으로 axios 라이브러리를 설치합니다. 이 정보를 사용하여 요청합니다.


yarn add axios

그런 다음 src / contact-list-page.js를 업데이트하여 데이터 가져 오기 요청을 수행하고 해당 결과를 사용하여 전역 상태를 업데이트합니다. 더 이상 필요하지 않으므로 정적 데이터 배열 목록을 제거해야 합니다. 다음과 같이 코드를 업데이트하십시오.


// src/contact-list-page.js

import React, { useContext, useEffect } from 'react';
import axios from 'axios';
import ContactList from '../components/contact-list';
import { ContactContext } from '../context/contact-context';

const ContactListPage = () => {
  const [state, dispatch] = useContext(ContactContext);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get('http://localhost:3030/contacts');
      dispatch({
        type: 'FETCH_CONTACTS',
        payload: response.data.data || response.data, // in case pagination is disabled
      });
    };
    fetchData();
  }, [dispatch]);

  return (
    <div>
      <h1>List of Contacts</h1>
      <ContactList contacts={state.contacts} />
    </div>
  );
}

export default ContactListPage;

저장 후 브라우저로 돌아갑니다. 이제 연락처 목록 페이지에 데이터베이스의 데이터가 표시됩니다.


오류 처리 


백엔드 서버와 Mongo 데이터베이스 서비스를 시작하는 것을 잊었다 고 가정 해 보겠습니다. create-react-app 서버를 실행하면 홈 페이지에 연락처가 표시되지 않습니다. 콘솔 탭을 열지 않는 한 오류가 발생했음을 나타내지 않습니다.


먼저 오류 메시지를 표시 할 구성 요소를 만들어 오류 처리를 구현해 보겠습니다. 또한 포착 된 오류에서 정보를 추출하기 위한 도우미 기능을 구현합니다. 이 도우미 기능은 네트워크 오류와 백엔드 서버에서 보낸 오류 메시지 (예 : 유효성 검사 또는 404 오류 메시지)를 구분할 수 있습니다.


Semantic UI React의 메시지 구성 요소를 사용하여 코드를 작성합니다. src / components 폴더에 flash-message.js 파일을 만듭니다.


touch src/components/flash-message.js

그런 다음 다음 코드를 삽입하십시오.


// src/components/flash-message.js

import React from 'react';
import { Message } from 'semantic-ui-react';

export const FlashMessage = ({ message }) => {
  return (
    <Message
      positive={message.type === 'success'}
      negative={message.type === 'fail'}
      header={message.title}
      content={message.content}
    />
  );
}

export const flashErrorMessage = (dispatch, error) => {
  const err = error.response ? error.response.data : error; // check if server or network error
  dispatch({
    type: 'FLASH_MESSAGE',
    payload: {
      type: 'fail',
      title: err.name,
      content: err.message,
    },
  });
}

다음으로,이 감속기를 src / context / contact-context.js에 추가하여 플래시 메시지를 처리합니다.


function reducer(state, action) {
  switch  (action.type)  {
    ...
    case 'FLASH_MESSAGE': {
      return {
        ...state,
        message: action.payload,
      };
    }
    ...
  }
}

마지막으로 pages / contact-list-page.js를 업데이트합니다. 오류를 포착하고 전달하기 위한 try… catch 메커니즘을 구현할 것입니다. 또한 FLASH_MESSAGE가 전달 된 경우에만 표시되는 FlashMessage 구성 요소를 렌더링 합니다.


// src/pages/contact-list-page.js

...
import { FlashMessage, flashErrorMessage } from '../components/flash-message';

const ContactListPage = () => {
  const [state, dispatch] = useContext(ContactContext);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('http://localhost:3030/contacts');
        dispatch({
          type: 'FETCH_CONTACTS',
          payload: response.data.data || response.data, // in case pagination is disabled
        });
      } catch (error) {
        flashErrorMessage(dispatch, error);
      }
    };
    fetchData();
  }, [dispatch]);

  return (
    <div>
      <h1>List of Contacts</h1>
      {state.message.content && <FlashMessage message={state.message} />}
      <ContactList contacts={state.contacts} />
    </div>
  );
}

export default ContactListPage;

다음은 백엔드 서버가 실행 중이지만 Mongo 데이터베이스 서비스가 중지되었을 때 발생하는 오류 메시지의 스크린 샷입니다.


error message example 

위의 오류에서 복구하려면 먼저 Mongo 서비스를 시작한 다음 Feathers 백엔드 서버를 순서대로 시작해야 합니다.


React Hook 양식을 사용하여 생성 요청 처리 


다음으로 새 연락처를 추가하는 방법을 살펴보고 이를 수행하려면 양식이 필요합니다. 처음에는 양식 작성이 매우 쉬워 보입니다. 그러나 클라이언트 측 유효성 검사에 대해 생각하고 오류가 표시되어야하는시기를 제어하기 시작하면 까다로워집니다. 또한 백엔드 서버는 자체 유효성 검사를 수행하므로 양식에 이러한 오류도 표시해야 합니다.


모든 양식 기능을 직접 구현하기보다는 양식 라이브러리 (React Hook Form)의 도움을 받을 것입니다. 이는 제 생각에 React 양식을 만들 때 사용하기 가장 쉬운 라이브러리입니다. 또한 클래스 이름 패키지를 사용하여 유효성 검사 오류가 있는 양식 필드를 강조 표시합니다.


먼저 Ctrl + C를 사용하여 create-react-app 서버를 중지하고 다음 패키지를 설치합니다.


yarn add react-hook-form classnames

패키지 설치가 완료된 후 서버를 다시 시작하십시오.


이 CSS 클래스를 src / index.css 파일에 추가하여 양식 오류의 스타일을 지정합니다.


.error {
  color: #9f3a38;
}

그런 다음 src / components / contact-form.js를 열어 양식 사용자 인터페이스를 빌드하십시오. 다음과 같이 기존 코드를 바꿉니다.


// src/components/contact-form.js

import React, { useContext } from 'react';
import { Form, Grid, Button } from 'semantic-ui-react';
import { useForm } from 'react-hook-form';
import classnames from 'classnames';
import { ContactContext } from '../context/contact-context';

const ContactForm = () => {
  const [state] = useContext(ContactContext);
  const { register, errors, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <Grid centered columns={2}>
      <Grid.Column>
        <h1 style={{ marginTop: '1em' }}>Add New Contact</h1>
        <Form onSubmit={handleSubmit(onSubmit)} loading={state.loading}>
          <Form.Group widths="equal">
            <Form.Field className={classnames({ error: errors.name })}>
              <label htmlFor="name.first">
                First Name
                <input
                  id="name.first"
                  name="name.first"
                  type="text"
                  placeholder="First Name"
                  ref={register({ required: true, minLength: 2 })}
                />
              </label>
              <span className="error">
                {errors.name &&
                  errors.name.first.type === 'required' &&
                  'You need to provide First Name'}
              </span>
              <span className="error">
                {errors.name &&
                  errors.name.first.type === 'minLength' &&
                  'Must be 2 or more characters'}
              </span>
            </Form.Field>
            <Form.Field>
              <label htmlFor="name.last">
                Last Name
                <input
                  id="name.last"
                  name="name.last"
                  type="text"
                  placeholder="Last Name"
                  ref={register}
                />
              </label>
            </Form.Field>
          </Form.Group>
          <Form.Field className={classnames({ error: errors.phone })}>
            <label htmlFor="phone">
              Phone
              <input
                id="phone"
                name="phone"
                type="text"
                placeholder="Phone"
                ref={register({
                  required: true,
                  pattern: /^\+(?:[0-9] ?){6,14}[0-9]$/,
                })}
              />
            </label>
            <span className="error">
              {errors.phone &&
                errors.phone.type === 'required' &&
                'You need to provide a Phone number'}
            </span>
            <span className="error">
              {errors.phone &&
                errors.phone.type === 'pattern' &&
                'Phone number must be in International format'}
            </span>
          </Form.Field>
          <Form.Field className={classnames({ error: errors.email })}>
            <label htmlFor="email">
              Email
              <input
                id="email"
                name="email"
                type="text"
                placeholder="Email"
                ref={register({
                  required: true,
                  pattern: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
                })}
              />
            </label>
            <span className="error">
              {errors.email &&
                errors.email.type === 'required' &&
                'You need to provide an Email address'}
            </span>
            <span className="error">
              {errors.email &&
                errors.email.type === 'pattern' &&
                'Invalid email address'}
            </span>
          </Form.Field>
          <Button primary type="submit">
            Save
          </Button>
        </Form>
      </Grid.Column>
    </Grid>
  );
}

export default ContactForm;

시간을 내어 코드를 검토하십시오. 거기에서 많은 일이 일어나고 있습니다. React Hook Form의 작동 방식을 이해하려면 시작 안내서를 참조하십시오. 또한 Semantic UI React의 Form 문서를 살펴보고 이를 사용하여 양식을 구축 한 방법을 확인하십시오. onSubmit 핸들러에서 양식 데이터를 콘솔에 출력하고 있습니다.


이제 브라우저로 돌아가서 의도적으로 불완전한 양식을 저장해 보겠습니다. 앞서 설정 한 탐색 메뉴를 사용하여 연락처 추가 버튼을 클릭 한 다음 양식을 채우지 않고 저장 버튼을 누릅니다. 그러면 다음 유효성 검사 오류 메시지가 트리거 됩니다.


Client-side validation errors 

이제 양식 작성을 시작할 수 있습니다. 입력하는 동안 다양한 유효성 검사 메시지가 변경되거나 사라집니다. 모든 것이 유효하면 저장을 다시 누를 수 있습니다. 콘솔 출력을 확인하면 다음 구조와 유사한 JSON 객체를 가져와야 합니다.


{
  "name":{
    "first": "Jason",
    "last": "Bourne"
  },
  "phone": "+1 555 555",
  "email": "jason@gmail.com"
}

이제 데이터베이스에 새 연락처를 저장하는 데 필요한 작업을 정의하겠습니다. 먼저 CREATE_CONTACT에 대한 감속기 처리기를 지정하겠습니다. 다음과 같이 src / context / contact-context.js를 업데이트합니다.


function reducer(state, action) {
  switch  (action.type)  {
    ...
    case 'CREATE_CONTACT': {
      return {
        ...state,
        contacts: [...state.contacts, action.payload],
        message: {
          type: 'success',
          title: 'Success',
          content: 'New Contact created!',
        },
      };
    }
    ...
  }
}

다음으로 src / components / contact-form.js를 열고 다음과 같이 코드를 업데이트합니다.


import React, { useContext, useState } from 'react';
import { Form, Grid, Button } from 'semantic-ui-react';
import { useForm } from 'react-hook-form';
import classnames from 'classnames';
import axios from 'axios';
import { Redirect } from 'react-router-dom';
import { ContactContext } from '../context/contact-context';
import { flashErrorMessage } from './flash-message';

const ContactForm = () => {
  const [state, dispatch] = useContext(ContactContext);
  const { register, errors, handleSubmit } = useForm();
  const [redirect, setRedirect] = useState(false);

  const createContact = async data => {
    try {
      const response = await axios.post('http://localhost:3030/contacts', data);
      dispatch({
        type: 'CREATE_CONTACT',
        payload: response.data,
      });
      setRedirect(true);
    } catch (error) {
      flashErrorMessage(dispatch, error);
    }
  };

  const onSubmit = async data => {
    await createContact(data);
  };

  if (redirect) {
    return <Redirect to="/" />;
  }

  return (
    //... form code
  )
}

export default ContactForm;

새 연락처 생성을 처리하기 위해 별도의 createContact 함수를 만들었습니다. 나중에 기존 연락처를 업데이트 하기 위한 다른 기능을 구현할 것입니다. 네트워크 오류 든 서버 오류 든 오류가 발생하면 사용자에게 무엇이 잘못되었는지를 알려주는 플래시 메시지가 표시됩니다. 그렇지 않으면 POST 요청이 성공하면 / 로의 리디렉션이 수행됩니다. 그러면 홈 페이지에 성공 메시지가 표시됩니다.


이제 양식 작성을 마칩니다. 저장을 클릭하면 목록 페이지로 이동해야 합니다. 아래 예에서는 연락처를 두 개 더 추가했습니다.


contact list with three contact cards 


기존 연락처 편집 


이제 새 연락처를 추가 할 수 있으므로 기존 연락처를 업데이트하는 방법을 살펴 보겠습니다. 단일 연락처를 가져오고 연락처를 업데이트 하기 위한 두 개의 감속기를 정의하여 시작하겠습니다.


다음과 같이 src / context / contact-context.js를 업데이트합니다.


function reducer(state, action) {
  switch  (action.type)  {
    ...
    case 'FETCH_CONTACT': {
      return {
        ...state,
        contact: action.payload,
      };
    }
    case 'UPDATE_CONTACT': {
      const contact = action.payload;
      return {
        ...state,
        contacts: state.contacts.map(item =>
          item._id === contact._id ? contact : item,
        ),
        message: {
          type: 'success',
          title: 'Update Successful',
          content: `Contact "${contact.email}" has been updated!`,
        },
      };
    }
    ...
  }
}

다음으로 ContactCard 구성 요소의 편집 버튼을 사용자를 양식으로 안내하는 링크로 변환 해 보겠습니다.


// src/components/contact-card.js

...
import { Link } from 'react-router-dom';

const ContactCard = ({ contact }) => {
  return (
    <Card>
      ...
      <Card.Content extra>
        <div className="ui two buttons">
          <Button
            basic
            color="green"
            as={Link}
            to={`/contacts/edit/${contact._id}`}
          >
            Edit
          </Button>
          <Button basic color="red">
            Delete
          </Button>
        </div>
      </Card.Content>
    </Card>
  );
}

export default ContactCard;

이제 사용자가 편집 버튼을 클릭하면 URL이 http : // localhost : 3030 / contacts / edit / {id}로 변경됩니다. 현재 ContactFormPage 구성 요소는 이러한 URL을 처리하도록 구축되지 않았습니다. src / pages / contact-form-page.js 파일의 기존 코드를 다음으로 대체하겠습니다.


import React, { useContext, useEffect, useState } from 'react';
import axios from 'axios';
import ContactForm from '../components/contact-form';
import { flashErrorMessage } from '../components/flash-message';
import { ContactContext } from '../context/contact-context';

const ContactFormPage = ({ match }) => {
  const [state, dispatch] = useContext(ContactContext);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const { _id } = match.params; // Grab URL _id

    if (_id) {
      const fetchData = async () => {
        try {
          const response = await axios.get(
            `http://localhost:3030/contacts/${_id}`,
          );
          dispatch({
            type: 'FETCH_CONTACT',
            payload: response.data,
          });
          setLoading(false);
        } catch (error) {
          flashErrorMessage(dispatch, error);
        }
      };
      fetchData();
    } else {
      setLoading(false);
    }
  }, [match.params, dispatch]);

  if (loading) {
    return <p>Please wait...</p>;
  }

  return (
    <div>
      <ContactForm contact={state.contact} />
    </div>
  );
}

export default ContactFormPage;

페이지가 로드 되면 URL에 _id가 있는지 확인합니다. 없는 경우 새 연락처를 만드는 데 사용할 수 있는 빈 양식이 로드 됩니다. 그렇지 않으면 가져 오기 쿼리를 수행하고 dispatch 함수를 통해 state.contact를 채 웁니다.


기본적으로 true로 설정되는 로컬로드 상태도 지정했습니다. 이는 state.contact가 채워질 때까지 ContactForm 구성 요소의 렌더링을 지연하기 위한 것입니다. 지연이 필요한 이유를 이해하려면 src / components / contact-form.js를 열고 다음과 같이 코드를 업데이트하십시오.


...
const ContactForm = ({contact}) => {
  ...
  const { register, errors, handleSubmit } = useForm({
    defaultValues: contact,
  });
  ...
  const updateContact = async data => {
    try {
      const response = await axios.patch(
        `http://localhost:3030/contacts/${contact._id}`,
        data,
      );
      dispatch({
        type: 'UPDATE_CONTACT',
        payload: response.data,
      });
      setRedirect(true);
    } catch (error) {
      flashErrorMessage(dispatch, error);
    }
  };

  const onSubmit = async data => {
    if (contact._id) {
      await updateContact(data);
    } else {
      await createContact(data);
    }
  };
  ...
  return (
    //... Display Form Mode
    <h1 style={{ marginTop: "1em" }}>
      {contact._id ? "Edit Contact" : "Add New Contact"}
    </h1>
    ....
  );
}

export default ContactForm;

위에서 볼 수 있듯이 연락처 업데이트를 위한 새로운 기능을 도입했습니다. URL이 다르고 PATCH HTTP 요청을 사용한다는 점을 제외하면 createContact와 거의 동일합니다. 또한 양식의 제출 작업을 업데이트해야 하는지 아니면 만들어야 하는지 결정하기 위해 _id가 있는지 확인하고 있습니다.


로드 상태의 목적으로 돌아 가면 알다시피, React는 일반적으로 props를 통해 컴포넌트에 연결된 데이터가 변경되면 다시 렌더링 됩니다. 불행히도 기존 연락처를 React Hook Form에 전달하는 것은 초기화 중에 만 수행 할 수 있습니다. 즉, 양식이 처음 로드 될 때 가져 오기 기능이 비동기식이므로 비어 있습니다. state.contact 필드를 확인하고 채울 때까지 양식은 링크가 없기 때문에 빈 상태로 유지됩니다.


이 문제를 해결하는 한 가지 방법은 setValue 함수를 사용하여 각 필드의 값을 프로그래밍 방식으로 설정하는 함수를 작성하는 것입니다. 구현 한 다른 방법은 state.contact가 채워질 때까지 ContactForm 구성 요소의 렌더링을 지연하는 것입니다.


목록 페이지 새로 고침이 완료되면 연락처를 선택하고 편집 버튼을 누르십시오.




Edit form displaying an existing contact 

변경을 완료하고 저장을 누르십시오.


List of edited contacts 

이제 애플리케이션에서 사용자가 새 연락처를 추가하고 기존 연락처를 업데이트 할 수 있습니다.


삭제 요청 구현 


이제 최종 CRUD 작업 인 삭제를 살펴 보겠습니다. 이것은 코딩이 훨씬 간단합니다. src / context / contact-context.js 파일에 DELETE_CONTACT 리듀서를 구현하는 것으로 시작합니다.


function reducer(state, action) {
  switch (action.type) {
    ...
    case 'DELETE_CONTACT': {
      const { _id, email } = action.payload;
      return {
        ...state,
        contacts: state.contacts.filter(item => item._id !== _id),
        message: {
          type: 'success',
          title: 'Delete Successful',
          content: `Contact "${email}" has been deleted!`,
        },
      };
    }
    ...
  }
}

다음으로 실제 삭제를 수행하는 기능을 구현합니다. src / components / contact-card.js에서 이 작업을 수행합니다. 다음과 같이 업데이트하십시오.


...
import  axios  from  'axios';
import  { ContactContext }  from  '../context/contact-context';
import  { flashErrorMessage }  from  './flash-message';

const  { useContext }  =  React;

const ContactCard = ({ contact }) => {
  // eslint-disable-next-line no-unused-vars
  const [state, dispatch] = useContext(ContactContext);

  const deleteContact = async id => {
    try {
      const response = await axios.delete(
        `http://localhost:3030/contacts/${id}`,
      );
      dispatch({
        type: 'DELETE_CONTACT',
        payload: response.data,
      });
    } catch (error) {
      flashErrorMessage(dispatch, error);
    }
  };

  return (
    ...
     <Button basic color="red" onClick={() => deleteContact(contact._id)}>
       Delete
     </Button>
    ...
  );
}

export default ContactCard;

브라우저가 새로 고쳐질 때까지 기다린 다음 하나 이상의 연락처를 삭제 해보십시오. 삭제 버튼이 예상대로 작동해야 하며 상단에 확인 메시지가 표시됩니다.


문제로 사용자에게 삭제 작업을 확인하거나 취소하도록 요청하도록 삭제 버튼의 onClick 핸들러를 수정 해보세요.


결론 


이제 React 및 Feathers를 사용하여 CREATE, READ, UPDATE 및 DELETE 작업을 수행 할 수 있는 완전한 애플리케이션이 구축되었습니다. 이제 React 애플리케이션의 CRUD 로직을 이해 했으므로 기술을 자유롭게 대체 할 수 있습니다. 예를 들어 Bulma, Materialize 또는 Bootstrap과 같은 다른 CSS 프레임 워크를 사용할 수 있습니다. LoopBack과 같은 다른 백엔드 서버 또는 Strapi와 같은 헤드리스 CMS 플랫폼을 사용할 수도 있습니다.


또한 우리가 작성한 코드가 여러면에서 개선 될 수 있음을 지적하고 싶습니다. 예를 들어 다음을 수행 할 수 있습니다.


  • 하드 코딩 된 URL을 환경 변수로 대체
  • 특정 위치에서 코드를 리팩터링하여 더 깔끔하게 만들기
  • 주석을 통해 문서 추가
  • 도의 파일에 감속기 코드 구현
  • 작업 파일을 만들고 모든 가져 오기 관련 코드를 거기에 배치하십시오.
  • 사용자 친화적인 메시지를 구현하여 오류 처리 개선
  • 최신 테스트 프레임 워크를 사용하여 단위 및 종단 간 테스트 작성


#이 작업을 하지 않기로 결정하고 대신 사용 중인 위치 옆에 작업 코드를 배치 할 수 있습니다. 그러나 여러 곳에서 액션 코드를 호출 할 수 있는 상황이 있습니다. 이 경우 이러한 코드를 공유 가능한 작업 파일로 이동하는 것이 좋습니다.



GraphQL은 REST API를 대체하는 최신 기술입니다. 프런트 엔드 개발자는 조인 된 레코드를 쿼리 할 수 ​​있습니다. JOIN SQL / 비 SQL 쿼리를 실행하는 커스텀 경로를 작성하지 않으면 REST API로 레코드를 결합 할 수 없습니다. Feathers는 fgraphql 후크를 통해 GraphQL을 지원하므로 프런트 엔드 인터페이스에서 GraphQL 사용을 쉽게 시작할 수 있습니다.


Next.js는 create-react-app에서 가능한 것보다 더 나은 SEO와 웹 사이트 성능을 제공하는 서버 렌더링 프레임 워크입니다. 이러한 기술, Next.js 및 Feathers와 GraphQL 지원을 결합하면 적은 노력으로 강력한 데이터 관리 애플리케이션을 구축 할 수 있습니다.