본문 바로가기
Web Study/Next.js

Next.js로 SNS x.com 클론코딩하기 - 4

by 쿠리의일상 2024. 1. 24.

섹션 3

지난 시간까지 최대한 x와 일치하게 클론 코딩을 진행하였습니다. 이번 섹션은 디자인 부분을 마쳤지만 API 명세서가 아직 없을 경우를 가정하고 진행됩니다.

 

MSW 로 임시 API 만들기

https://mswjs.io/

 

Mock Service Worker

API mocking library for browser and Node.js

mswjs.io

그간 API는 직접 서버 안에 간단히 만들어서 Postman 으로 확인하거나 공공API만 사용했는데 해당 사이트에선 임시로 API를 만들 수 있다고 합니다. DB를 거치지 않고 요청에 대한 응답을 간단하게 예측하여 만드는 것이라 빠른 작업이 가능할 것 같습니다. 작업 전에 백엔드 개발자와 간단하게 소통하여 json 형태로 어떻게 올 것인지 얘기를 나눠보면 좋다고 합니다.

설치하기

npx msw init public/ --save
npm i -D @mswjs/http-middleware express cors

npm i --save-dev @types/express @types/cors

강의 내용 상 넥스트에서 MSW가 조금의 문제가 있다고 하는데, 해당 부분은 차근히 정리하겠습니다. 설치가 완료되면 public 폴더 안에 해당 파일이 생긴 것을 확인할 수 있습니다.

이 파일의 용도는 서버 주소로 API 요청을 보낼 때, MSW 측에서 해당 요청을 가로채는 역할을 한다고 합니다. 해당 라이브러리가 없었다면 process.env.NODE_ENV 에 접근하여 development or production 여부를 확인하고 개발용 주소인지 배포용 주소인지 분기를 나눠 요청을 처리해줬어야 하는데 이 과정을 생략할 수 있게 된 것입니다. MSW를 사용하여 실제 배포 주소를 가지고 개발 테스팅을 할 수 있게 되었습니다.

 

그다음 src 폴더 안에 mocks 폴더를 새로 만들어줍니다. (src 폴더가 없다면 app 폴더와 동일한 위치)

넥스트는 기본적으로 클라/서버가 같이 돌아가게 됩니다. 그런데 msw가 실행되려면 클라/서버쪽에서 설정을 해줘야합니다. 클라이언트의 경우 browser.ts, 서버의 경우 http.ts 파일을 만들고 아래처럼 설정해줍니다.

mocks/browser.ts

import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

const worker = setupWorker(...handlers);
export default worker;
mocks/http.ts

import { createMiddleware } from '@mswjs/http-middleware';
import express from 'express';
import cors from 'cors';
import { handlers } from './handlers';

const app = express();
const port = 9090;

app.use(cors({ origin: 'http://localhost:3000', optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () => console.log(`Mock server is running on port: ${port}`));

다만 여기서 위에 기재했듯 넥스트 서버와 msw 서버가 호환이 잘 안되므로 임시로 위의 코드처럼 node express 서버를 활용해준다고 합니다.

이제 이 두 파일에 들어가는 handlers 파일에다가 임시 API를 만듭니다.

handlers 파일 안에는 기존 1버전까지는 rest 를 가져와 요청 메서드를 실행해줬는데, 2버전 이후로 http 로 바뀌었다고 합니다. 

import {rest} from 'msw';

export const handlers = [
  rest.post('api/login', (req, res, ctx) => {
    return res(
      ctx.cookie('connect.sid', 'msw-cookie', {
        httpOnly: true,
        path: '/',
      }),
      ctx.json({
        userId: 1,
        nickname: '제로초',
        id: 'zerocho',
        image: '/5Udwvqim.jpg',
      })
    );
  }),

  rest.post('/api/logout', (req, res, ctx) => {
    return res(
      ctx.cookie('connect.sid', '', {
        httpOnly: true,
        path: '/',
      })
    );
  }),
];

강의에서 알려준 코드는 위와 같지만 마이그레이션이 필요합니다.

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.post('api/login', () => {
    return HttpResponse.json(
      {
        userId: 1,
        nickname: '제로초',
        id: 'zerocho',
        image: '/5Udwvqim.jpg',
      },
      {
        headers: {
          'Set-Cookie': 'connect.sid=msw-cookie;HttpOnly;Path=/',
        },
      }
    );
  }),

  http.post('/api/logout', () => {
    return new HttpResponse(null, {
      headers: {
        'Set-Cookie': 'connect.sid=;HttpOnly;Path=/;Max-Age=0',
      },
    });
  }),
];

바뀐 http 와 HttpResponse 가 좀더 덜 복잡하고 직관적인 느낌입니다.

 

 

Cookie

웹/브라우저 쿠키로, 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각을 의미

기본적으로 Http 통신은 stateless 하므로 매 요청마다 클라이언트를 확인해주기 위해 쿠키가 생긴 것입니다.

  • 세션 관리
  • 개인 설정 유지
  • 사용자 행동을 기록, 분석

등의 용도로 사용됩니다.

 

Http 쿠키의 특징으로는

  • 한 개에 4KB까지 저장 가능, 최대 300개까지 저장할 수 있는 텍스트 파일
  • 클라이언트 측에 저장
  • 만료날짜가 없는 경우 브라우저를 종료하면 사라짐(만료날짜가 없는 쿠키를 세션 쿠키라고 하며 그 반대는 영속 쿠키라고 합니다)

 

요청 헤더에서의 Cookie

Http 요청 시 클라이언트에서 서버로 전달하는 쿠키 헤더로, 세션 상태를 담은 세션ID나 클라이언트 정보를 가지고 있습니다.

응답 헤더에서의 Set-Cookie

서버에서 클라이언트로 전달하는 쿠키 헤더로, 클라이언트 해당 정보를 활용하여 쿠키를 만든 뒤 브라우저에 저장합니다.

 

쿠키는 여러 개의 데이터를 콤마로 열거해줍니다. 유효기간이나 도메인 등의 파라미터는 세미콜론을 사용하여 열거해줍니다. Http Cookie 파라미터의 종류는 아래와 같습니다.

  • 쿠키명: name=value
  • 유효기간(생명주기): expires=date
  • 쿠키 적용 대상 디렉토리: path=경로
  • 도메인명: domain=도메인
  • HTTPS로 통신하는 경우: Secure
  • 쿠키를 자바스크립트에서 액세스 못하도록 제한(해커의 XSS 공격 방지): HttpOnly

먼저 쿠키의 경로를 부연설명 하자면, 지정해준 경로에서만 쿠키 사용 접근이 가능하게 지정해줍니다. 해당 경로의 하위 경로까지 모두 포함하므로 루트 경로('/')를 설정하면 모든 경로에서 쿠키가 유효하게 되는 것입니다. 기본값이 루트 경로이므로 생략되어도 됩니다. 보안상 쿠키의 범위를 좁게 설정하는 것이 좋다고 합니다.

쿠키의 도메인의 경우, 해당 도메인 페이지에서 쿠키 사용 접근이 가능하게 제한을 주는 것으로 서브 도메인까지 모두 포함해줍니다. 도메인을 생략하면 현재 문서 기준 도메인만 적용됩니다.

 

Set-cookie: user=John; path=/; expires=Tue, 19 Jan 2038 00:00:00 GMT

위와 같은 쿠키가 있다면 user가 John인 루트 경로에서 유효하며 2038년 1월 19일까지 유효한 쿠키가 될 것입니다.

 

쿠키를 없애주려면

위의 마이그레이션의 쿠키는 'Set-Cookie': 'connect.sid=msw-cookie;HttpOnly;Path=/' 이렇게 설정해주고, 로그아웃 시 쿠키를 지워주기 위해 'Set-Cookie': 'connect.sid=;HttpOnly;Path=/;Max-Age=0' 으로 설정해주었습니다. 즉 쿠키를 없애주려면 name의 value 값을 없애주고, Max-Age=0 으로 설정해준다고 합니다.

 

MSW 실행 설정

package.json 안 scripts 명령어를 추가해줍니다. 별개의 터미널을 추가로 틀어 넥스트 서버와 별도로 구동 시켜주면 됩니다. 즉 클라이언트와 서버는 각각 따로 구동해줘야 하므로 클라이언트 --> 넥스트, 임시 서버 -- > MSW 가 되는 것입니다.

 

그다음 MSW 전용 컴포넌트를 만들어줘야 합니다. process.env.NEXT_PUBLIC_API_MOCKING 환경 변수가 enabled 일 때 실행될 수 있도록 아래와 같이 설정해줍니다.(이 환경 변수는 별도로 .env 파일을 만들어서 넣어줘야 합니다)

.env 

NEXT_PUBLIC_API_MOCKING=enabled

이 컴포넌트는 useEffect 훅을 사용해줘야 하므로 클라이언트 컴포넌트로 설정하고 로그인 전/후와 무관하게 홈페이지 어디서든 되게끔 app 폴더 하단에 만들어주고, 루트 레이아웃 파일의 body 태그 안에 넣어줍니다.

MSWComponent.tsx

'use client';

import { useEffect } from 'react';

export default function MSWComponent() {
  useEffect(() => {
    if (typeof window !== 'undefined') {
      if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
        require('@/mocks/browser');
      }
    }
  }, []);

  return null;
}

위의 컴포넌트를 간단하게 설명하자면 window가 undefined가 아닌, 즉 브라우저(클라이언트)에서 돌아간다는 조건하에 환경변수로 개발 환경일 때만 MSW가 돌아가게끔 설정해줍니다.

 

여기까지가 기본 설정이었습니다. 위의 설정이 끝나야 클라이언트 측에서 요청을 보내면 MSWComponent 가 실행되어, mockServiceWorker.js 가 요청을 가로채서 실행합니다. 이는 결국 http.ts 서버로 보내주고 handlers 안에 임시 API가 실행됩니다.

 

그럼 실제 백엔드가 완성되면 어떻게 해줘야할까요? 위에 정해준 환경 변수 NEXT_PUBLIC_API_MOCKING 값을 다른 값으로 변경시켜주면 됩니다.

.env

더 간단한 방법으로는 환경변수의 env 파일의 경우 기존의 .env는 배포, 개발 환경 둘다 돌아가고, .env.local 은 개발 환경에서만 돌아가는 환경 변수 파일입니다.

추가로 환경 변수 앞의 NEXT_PUBLIC_ 접두사는 브라우저에서 접근이 가능하기 때문에 app 폴더 안의 컴포넌트들에서 접근이 가능한 것입니다. 이는 개발자 도구를 통해서 볼 수 있으므로 중요한 정보는 저장하면 안됩니다.

NEXT_PUBLIC_ 접두사가 없을 경우 서버에서만 접근이 가능한 환경 변수가 됩니다. 서버에서만 확인 가능하므로 보안상 유리하겠죠?