리액트 개발에 있어서, 임시서버를 구축하는 방법은 여러가지가 있다. 그 중에 하나가 json-server이고, 다른 하나로 MSW가 있다. 여기서 간편한 것은 json-server이다. 루트디렉토리에 db.json 파일을 만들어주고, 로컬에서 실행 중인 리액트 포트와 다른 포트에서 실행해주면 되기 때문이다. 반면에 MSW는 통신을 갈취하여 단일 포트 안에서 작업이 이뤄지도록 하는 것이 차이이다. 즉 json-server는 외부 도구로서 간편하게 사용할 수 있는 반면에 MSW는 리액트 애플리케이션 내에서 통합하여 작업할 수 있다는 차이가 대표적이다.
yarn add msw
라이브러리를 설치했다면, 폴더트리를 아래와 같이 생성해준다.
src
├── mock
│ ├── browser.ts
│ ├── handlers.ts
└── ...
browser.ts: 이 파일은 MSW를 브라우저 환경에서 실행하기 위한 설정 파일이다. 주로 개발 환경에서 사용되며, browser.ts 파일은 MSW의 setupWorker 함수를 호출하여 Mock Service Worker를 설정하고, 브라우저의 serviceWorker 등록을 수행한다.
import { setupWorker } from "msw";
import { listshandlers } from "./listshandlers";
export const worker = setupWorker(...listshandlers)
handlers.ts: 이 파일은 실제 API 요청을 가로채고 처리하는 MSW 핸들러 함수들이 정의되는 곳이다. 각 핸들러 함수는 특정 API 엔드포인트에 대한 요청을 가로채고 가짜 응답을 제공한다. 이를 통해 개발자는 실제 백엔드 API가 아직 구현되지 않은 상황에서도 가짜 데이터를 사용하여 애플리케이션을 개발하고 테스트할 수 있다. 헨들러는 복수가 될 수 있으며, 복수라면, 전개구문을 통하여, browser.ts에 등록해 주면 된다.
해당 폴더 안에 파일들을 만들었다면, 터미널을 통해서 아래의 명령어를 입력해준다.
yarn msw init public/ --save
터미널에서 메시지 확인 : Service Worker successfully created!
명령어를 선언하면, 아래와 같이 Mork 관련 파일이 생성되는 것을 확인할 수 있다.
pubilc
└── ...
├── mockServiceWorker.js
└── ...
다음으로는 index.tsx 파일에 가서 아래와 같은 코드를 넣어준다.
// Start the mocking conditionally.
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mork/browser')
worker.start()
}
해당라이브러리는 컴포넌트에서 요청되는 API를 중간에서 갈취하여 동작함으로 동일한 엔드포인트를 지정해줘야 동작이 이뤄진다.
import { rest } from "msw";
let lists: MainData[] = [
{
id: 1,
title: "(1) 타입스크립트로, 리액트 컴포넌트 제어하기",
desc: "이번에 타입스크립트 제대로 한 번 해보자.",
imgs: projectimgs01,
},
{
id: 2,
title: "(2) msw 목서버 만들기",
desc: "이번에 개인프로젝트 잘 만들어보자.",
imgs: projectimgs01,
},
{
id: 3,
title: "(3) React-Redux로 상태관리하기",
desc: "이번에 Redux 뚝배기 꺠보자!!!",
imgs: projectimgs01,
},
];
export const listshandlers = [
rest.get(`${process.env.REACT_APP_SERVER_KEY}/lists`, async (req, res, ctx) => {
return res(ctx.json({ lists }));
}),
];
사용방법은 위와 같다. 임시데이터(lists)를 선언해준다. msw에 등록된 엔드포인트로 클라이언트에서 요청이 들어오면, msw가 이를 갈취하여 통신을 임시서버로 이끌어 해당된 결과의 응답을 돌려준다. 이를 통해서 서벅가 개발되기 전에 프론트엔드 개발 테스트를 진행할 수 있다. API는 복수가 있을 수 있는데, 관련 API 하나당 [] 배열 안에 담아준다. 해당 핸들러 안에는 함수목록이 들어가게 되는데, 접두어 메서드는 rest(msw)이다.
HTTP 통신 과정에서 클라이언트와 서버는 특정한 데이터처리 방식으로 리소스를 주고받는데 그 가운데 대표적인 것인 json(JavaScript Object Notation) 형식이다. 그래서 반환을 할 때, json형식에 리소스를 담아서 보내는 것을 확인할 수 있다.
Mock Service Worker(MSW)에서 사용되는 ctx 객체의 약어이다. context의 줄임말로, 컨텍스트 객체를 사용하여 요청 및 응답에 대한 조작을 수행할 때 사영된다. ctx.json()은 res() 함수의 메서드 중 하나로, 해당 메서드를 사용하여 가짜 응답 데이터를 JSON 형식으로 설정한다.
ctx는 indeed Mock Service Worker(MSW)에서 사용되는 ctx 객체의 약어입니다. ctx는 context의 줄임말로, MSW에서 제공하는 컨텍스트 객체를 가리킵니다. 컨텍스트 객체를 사용하여 요청 및 응답에 대한 다양한 조작을 수행할 수 있습니다. ctx.json()은 res() 함수의 메서드 중 하나로, 해당 메서드를 사용하여 가짜 응답 데이터를 JSON 형식으로 설정합니다. 따라서 ctx는 MSW에서 사용되는 컨텍스트 객체를 의미하며, ctx.json()은 가짜 응답 데이터를 JSON 형식으로 설정하는 메서드입니다.
이해하기 좋은 용어로는 Payload가 있다. Payload는 일반적으로 데이터 전송 과정에서 실제로 전송되는 정보를 가리킨다. 따라서 ctx.json()의 결과물은 응답의 Payload로 이해할 수 있다.
클라이언트에서 서버로 데이터를 전송할 때, 이미지를 포함한 경우에는 formData 객체 안에 담아서 보내야 한다. 이때, headers의 설정을 통해서 데이터 전송파일 형식을 변경해주어야 하기도 한다. 이는 추후 다른 포스트에서 자세히 다룰 예정이다. 여기서는 개념만 정리하고 넘어하고자 한다.
FormData 객체란, 웹 브라우저에서 제공하는 JavaScript 객체이다. HTML form 요소들의 데이터를 쉽게 수집하고 전송하기 위해 사용된다. <form>
요소 내의 입력 필드들의 값을 FormData 객체에 추가하여 서버로 전송한다.
이미지의 경우에는 FormData 객체에 담기면, base64 로 인코딩된다. 해당 인코딩은 바이너리 데이터를 텍스트로 변환하는 인코딩 방식 중 하나이다. 이러한 변환 및 전송 방식을 이해하기 위해서는 이미지가 사용자로부터 받아졌을 때의 상태를 알아야 한다. 이미지는 8비트로 이루어진 이진 형태의 데이터이다. 이를 텍스트로 변환해야 하는데, 이를 위해서 사용되는 인코딩 방식이 base64인 것이다. 6비트씩 데이터를 처리하여 일련의 문자 집합에 매핑한다. 이 과정을 통해서 변환된 텍스트는 일반적인 텍스트 형식으로 전송할 수 있으며 수신 측에서 텍스트 데이터를 디코딩하여 다시 바이너리 데이터로 변환할 수 있다. 이때 파일의 크기가 원본 대비 1/3 정도 더 커진다. 그럼에도 해당 방식을 채택하는 것은 텍스트로 전송할 수 있는 장점이 있기 때문이다.
GET 메서드나 POST 메시지 까지는 공식문서에서 쉽게 확인할 수 있었다. 그러나 DELET 나 PATCH 메서드는 확인하기 어려워서 고생을 했던 것 같다. 관련 내용들을 아래에 기록한다.
// POST 요청 핸들러
rest.post<MainData>(`${process.env.REACT_APP_SERVER_KEY}/lists`, async (req, res, ctx) => {
const newItem: MainData = await req.json()
lists.push(newItem)
return res(
ctx.status(200),
)
}),
타입스트립트로 프로젝트를 하고 있기에, 타입정의를 해줘야 했었다. 관련 부분이 제네릭으로 기록된 MainData 부분이다. 어떤 형식이 들어올 지 모르기에, 제안된 제네릭 안에 들어올 데이터의 타입을 정의해주었다. 클라이언트에서 전송된 데이터는 json 파일로 전달된다. ( req.json() ) 해당 내용을 변수에 담아서 기존의 배열에 추가해주면 된다. 그리고 컴포넌트의 프로미스를 통해서 .then을 활용해서 새롭게 get메서드를 호출하면 추가된 데이터가 화면에 그려지는 것을 확인해볼 수 있다.
// DELETE 요청 핸들러
rest.delete(`${process.env.REACT_APP_SERVER_KEY}/lists/:id`, async (req, res, ctx) => {
const id = parseInt(req.params.id as string);
if (id) {
const listsIndex = lists.findIndex(items => items.id === id);
lists.splice(listsIndex, 1)
return res(ctx.status(200))
} else {
return res(ctx.status(400))
}
})
데이터를 삭제하는 방법은 간단하다. 데이터에 접근할 수 있는 데이터의 id 값을 엔드포인트로부터 추출하면 된다. 이때 사용할 수 있는 메서드가 req.params.id 이다. 이때 해당 부분의 타입은 언제나 string이다. 그러기에 이를 위한 타입정의를 해주어야 하는데, 해당 부분이 타입추론을 통해서 정의되어 있었기에, 명시적으로 선언해줄 필요가 있었다. 이를 위해서 타입단언(as)을 통해서 타입을 정의해주었다. 그러나 타입단언보다는 타입가드가 선호되는 방식이기에 해당 부분을 공부하고 여기에 정의해야 될 것 같다.
이후는 원본 리소스(lists)에서 해당 id를 가진 객체를 찾고, 이를 배열에서 제거하는 것이다. 여기에서는 findIndex를 통해서 관련 프로퍼티의 인덱스를 추출하고, 이를 splice를 통하여 제거함으로 처리를 했다.
CRUD의 마지막은 수정에 대한 부분이다. 관련 부분은 클라이언트 측에서도 설정할 때 까다롭지만, msw의 설정에서도 가장 어려운 측면이다. 일단 코드가 제일 길다.
// PATCH 요청 핸들러
rest.patch<MainData>(`${process.env.REACT_APP_SERVER_KEY}/lists/:id`, async (req, res, ctx) => {
const id = parseInt(req.params.id as string);
const updatePayload: MainData = await req.json()
let findItem;
findItem = lists.find(item => item.id === id)
if(findItem) {
findItem.title = updatePayload.title;
findItem.desc = updatePayload.desc;
return res(
ctx.status(200),
)} else {
return res(
ctx.status(400),
)
}
}),
일종의 POST와 DELETE 메서드의 결합이라고 여겨도 좋을 것 같다. id를 통해서 기존 배열에서 수정해야 되는 프로퍼티를 찾고, 관련 프로퍼티를 찾으면, 해당 내용에 수정할 부분을 덮어씌어주면 된다. 이때 조건분기를 통해서 id에 대한 내용이 있을 때와 그렇지 않을 때를 구분하여 처리하였다. 이렇게 msw 라이브러리를 통해서 임시서버를 구축해보았다.
관련 함수와 컴포넌트를 단일한 파일에 기록했을 때는 문제가 없었다. 그러나 로직 단위로 커스컴훅으로 분리하고, 컴포넌트를 분리했을 때, 변경된 상태에 대해서 컴퍼넌트가 인지하지 못하는 상황이 발생했다. 이를 위해서는 어떻게 해야할까? 바로 전역상태관리이다. Redux, Recoil, React-query 등의 다양한 방법들이 있을 것이다. 이번 개인프로젝트에서는 redux에 대한 연습이 필요함으로, redux로 관련 부분을 제어하고자 한다.