쇼핑몰프로젝트(9) - 회원가입, 로그인 (카카오주소검색, 유효성검사, MSW로 모의 API응답 )

신혜원·2023년 9월 16일
0
post-thumbnail

map 함수와 key

자꾸 콘솔창에 key값을 설정하라는 경고장이 떠서 map함수와 key에 대해 알아보았다.

우선 map 함수

const IterationSample = ()=>{
const names = ['눈사람', '얼음', '눈', '바람'];
const nameList = names.map(name=><li>{name}</li>);
return <ul>{nameList}</ul>
}

이렇게 자바스크립트 배열 객체의 내장함수인 map함수를 사용하여 반복되는 컴포넌트를 렌더링 할 수 있다.
map 함수는 파라미터로 전달된 함수를 사용하여 배열 내 각 요소를 원하는 규칙에 따라 변환한 후 그 결과로 새로운 배열을 생성한다.

key값

리액트에서 key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용한다.
유동적인 데이터를 다룰 때에는 key가 없다면 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지한다.
하지만! key값이 있다면? 이 값을 이용하여 어떤 변화가 일어났는지 더욱 빠르게 알아낼 수 있다.

key 설정

key값을 설정할때에는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯 설정하면 된다.
key값은 언제나 유일해야한다. 따라서 데이터가 가진 고유값을 key값으로 설정해야한다.

const articleList = articles.map(article =>(
  <Article
  	title={article.title}
	writer={article.writer}
	key={article.id}
  />

게시판의 게시물을 렌더링 한다면 이렇게 게시물 번호를 key값으로 설정해야한다.

만약 고유 번호가 없다면? 이때는 map함수에 전달되는 콜백함수의 인수인 index값을 사용하면 된다.

 const IterationSample = ()=>{
    const names = ['눈사람', '얼음', '눈', '바람'];
    const nameList = names.map(( name, index )=><li key={index}>{name}</li>);
    return <ul>{nameList}</ul>
  }

하지만 고유한 값이 없을때만 index값을 사용해야한다.
index를 key로 사용하면 배열이 변경될 때 효율적으로 리렌더링하지 못한다고 한다.


카카오 주소 검색

설치

npm install react-daum-postcode

import

import DaumPostcode from "react-daum-postcode";

주소 검색 완료 함수 생성

  • onComplete 함수에 주소 검색 완료 시 실행시킬 로직을 바인딩한다.
  • Address타입의 주소(data)를 인자로 받을 수 있다.
    (Address 타입은 react-daum-postcode에서 import 한다.)
  • 완료 함수 실행 시 주소창은 자동으로 사라진다.
  • data예시

    실제 반환되는 데이터 확인하기

사용해보기

나는 주소찾기를 모달창으로 사용했다. (대체적으로 모달창으로 띄워지기 때문이다.)

  const [formData, setFormData] = useState({
    name: "",
    userId: "",
    password: "",
    confirmPassword: "",
    phoneNumber: "",
    address: "",
  });

  const [showPost, setShowPost] = useState(false);

  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const handleComplete = (data) => {
    let fullAddress = data.address;
    setFormData({ ...formData, address: fullAddress });
    setShowPost(false); // 주소 검색 컴포넌트 숨기기
  };

// 회원가입 폼
<div className="signup-container">
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="name"
          placeholder="이름"
          value={formData.name}
          onChange={handleChange}
        />
        <input
          type="text"
          name="userId"
          placeholder="아이디"
          value={formData.userId}
          onChange={handleChange}
        />
        <input
          type="password"
          name="password"
          placeholder="비밀번호"
          value={formData.password}
          onChange={handleChange}
        />
        <input
          type="password"
          name="confirmPassword"
          placeholder="비밀번호 확인"
          value={formData.confirmPassword}
          onChange={handleChange}
        />
        <input
          type="text"
          name="phoneNumber"
          placeholder="전화번호"
          value={formData.phoneNumber}
          onChange={handleChange}
        />
        <div className="address-container">
          <input
            type="text"
            name="address"
            placeholder="주소"
            value={formData.address}
            onChange={handleChange}
          />
          <button type="button" onClick={() => setShowPost(true)}>
            주소 찾기
          </button>
        </div>
        <button type="submit">회원가입하기</button>
      </form>
      {/* 주소 찾기 모달창 */}
      {showPost && (
        <div
          className="modal"
          onClick={(e) => {
            if (e.target.className === "modal") {
              setShowPost(false);
            }
          }}
        >
          <div className="modal-content">
            <DaumPost handleComplete={handleComplete} />
          </div>
        </div>
      )}
(DaumPost.js)
// 주소찾기 모달창

import DaumPostCode from "react-daum-postcode";

function DaumPost({ handleComplete }) {
  const handleAddressSelection = (data) => {
    let fullAddress = data.address;
    let extraAddress = "";

    const { addressType, bname, buildingName } = data;
    if (addressType === "R") {
      if (bname !== "") {
        extraAddress += bname;
      }
      if (buildingName !== "") {
        extraAddress += `${extraAddress !== "" && ", "}${buildingName}`;
      }
      fullAddress += `${extraAddress !== "" ? ` ${extraAddress}` : ""}`;
    }
    handleComplete({ address: fullAddress });
  };
  return (
    <DaumPostCode onComplete={handleAddressSelection} className="post-code" />
  );
}

export default DaumPost;

DaumPost.js에서
클릭한 주소를 fullAddress에, 나머지 주소를 "" 빈칸으로 선언한다.
받은 data에서 { addressType, bname, buildingName } 을 가져오고 기본주소 타입이 도로명(R)일 때, 법정동/법정리 이름(bname) 이 채워져있다면 extraAddress에 bname을,
만약 건물명(buildingName)이 채워져있고, extraAddress가 빈칸이면 그 뒤에 "," 콤마를 적은 후 건물명을 적고, 빈칸이면 바로 뒤에 건물명을 적는다.
그리고 fullAddress와 extraAddress를 합쳐준 후 handleComplete(부모 컴포넌트에서 가져옴)에 {address : fullAddress} 객체를 넣어준다.

(Join.js)
  const handleComplete = (data) => {
    let fullAddress = data.address;
    setFormData({ ...formData, address: fullAddress });
    setShowPost(false); // 주소 검색 완료 시 컴포넌트 숨기기
  };

이 때 handleComplete에 들어간 data는 아까 전달받은
{address : fullAddress} 가 된다.


유효성 검사

(validateInput.js)
function validateInput(formData) {
  console.log(formData);
  const errors = [];
  // 공백이 아닌 문자가 있는지 검사
  const nameReg = /\S/;

  // 5~12자의 알파벳 및 숫자 (특수문자 사용하지 않음)
  const idReg = /^[a-zA-Z0-9]{5,12}$/;

  // 최소 8자, 영어(대문자 또는 소문자), 숫자포함
  const passwordReg = /^(?=.*\d)(?=.*[a-zA-Z]).{8,}$/;

  // 10~11자리의 숫자
  const phoneNumberReg = /^\d{10,11}$/;

  if (!nameReg.test(formData?.name)) {
    errors.push("이름을 입력해주세요.");
  }
  if (!idReg.test(formData?.userId)) {
    errors.push("아이디는 5~12자의 알파벳 및 숫자로 이루어져야 합니다.");
  }
  if (!passwordReg.test(formData?.password)) {
    errors.push(
      "비밀번호는 최소 8자리이며, 영어(대문자 또는 소문자), 숫자를 포함해야 합니다."
    );
  }
  if (formData?.password !== formData?.confirmPassword) {
    errors.push("비밀번호와 비밀번호 확인이 일치하지 않습니다.");
  }
  if (!phoneNumberReg.test(formData?.phoneNumber)) {
    errors.push("올바른 전화번호를 입력해주세요.");
  }
  if (!nameReg.test(formData?.address)) {
    errors.push("주소를 입력해주세요.");
  }

  if (errors.length > 0) {
    alert(errors[0]);
    return false;
  }

  return true;
}

export default validateInput;

validateInput함수에서는 각각 유효성을 const로 선언 후 if문을 사용해 formData의 값들과 비교 후 error 변수에 경고문을 넣는다.
경고문이 하나라도 있으면 alert 창에 첫번째로 들어온 error 메세지를 띄우고 false를, 없다면 true 를 반환한다.

(Join.js)
if (!validateInput(formData)) {
      return; // 유효성 검사 실패 시 함수 종료
    }

만약 validateInput이 false라면 유효성 검사 실패인 것으로 함수를 종료하게 만든다.

이 로직의 의도는
1. 사용자가 폼을 제출하려고 할 때 handleSubmit함수가 호출된다.
2. 함수 내에서 validateInput(formData)를 사용하여 제출된 데이터의 유효성을 확인한다.
3. 만약 데이터가 유효하지 않다면 함수는 즉시 종료되고, return;구문 때문에 나머지코드 (fetch를 사용하여 서버에 데이터를 전송하는 부분 등) 이 실행되지 않도록 한다. 바로 끝내버림!!
4. 데이터가 유효하다면 함수는 계속 실행되고 데이터를 서버에 전송한다.


MSW (Mock Service Worker)로 모의 API 응답

회원가입 후 회원정보를 로그인 할 때 사용할 수 있을까 싶어서 localStorage와 비교해보았다.

localStorage와 MSW 차이

localStorage
1. 저장목적 : localStorage는 웹 브라우저에서 제공하는 클라이언트 사이드 저장 메커니즘 중 하나이다.
2. 유지시간 : localStorage에 저장된 데이터는 영구적이다. 즉, 사용자가 브라우저의 탭을 닫거나 재시작해도 데이터가 남아있다.
3. 사용사례 : 세션관리, 사용자의 UI설정, 장바구니 정보, 최근 본 상품 등을 저장하는 데 유용하다.

MSW (Mock Service Worker)
1. 목킹목적 : MSW는 서비스 워커를 사용하여 네트워크 요청을 가로채고, 목킹된 응답을 반환하는 라이브러리이다.
즉, 실제 서버와의 통신 없이 프론트엔드에서 API 요청의 응답을 모방하는 것이 주된 목적이다.
2. 유지시간 : MSW는 어플리케이션의 런타임 중에만 네트워크 요청을 가로챈다.
3. 사용사례 : 백엔드 서버가 아직 준비되지 않았을 때 프론트엔드개발, 에러상황이나 특정상황을 재현하기 위한 테스트에서 사용된다.

따라서 회원정보를 저장하는 용으로 MSW는 적합하지 않고, 회원가입 요청을 MSW가 가로채고 모의 응답을 반환하는 데에 사용해야겠다.

사용해보기


mocks 폴더 안에 3개의 js파일을 만들어준다.

  • handlers.js : 실제 API 응답을 모방하는 핸들러 함수들을 정의하는 곳
  • server.js : Node.js 환경(주로 테스팅)에서 사용되는 가상 서버 설정을 담당한다.
  • worker.js : 브라우저 환경에서 작동하는 Service Worker를 설정하여 개발 환경에서의 API 요청을 가로챈다.
(server.js)
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);

(worker.js)
import { setupWorker } from "msw";
import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

핸들러 코드를 불러와서 그대로 setupWorker() 함수의 인자로 넘겨주면 된다.

요청 핸들러 작성

(handlers.js)

import { rest } from "msw";

export const handlers = [
  rest.post("/join", (req, res, ctx) => {
    // 여기서 req.body에는 회원가입 데이터가 있다.
    console.log("Received signup data:");
    return res(ctx.status(200), ctx.json({ message: "회원가입 성공!" }));
  }),

REST API를 모킹할 때는 msw모듈의 rest객체를 사용한다.

서비스워커 삽입하기
src/index.js파일 수정하기

import { worker } from "./mocks/worker";
if (process.env.NODE_ENV === "development") {
  worker.start();
}

일반적으로 개발환경에서만 가짜 API를 사용하므로 환경변수를 체크해서 선택적으로 서비스 워커가 구동되게 만들어준다.
msw로 백엔드 API 모킹하는 방법

try {
      const response = await fetch("/join", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(formData),
      });

      const data = await response.json();

      if (response.ok) {
        loginUser();
        alert(data.message); // "회원가입 성공!"
      } else {
        alert(data.error || "회원가입 실패");
      }
    } catch (error) {
      console.error("회원가입 에러:", error);
      alert("회원가입 중 문제가 발생했습니다.");
    }
  };

비동기작업(ex. API 호출) 중에 예상치 못한 오류나 예외 상황을 처리하기 위해 try/catch 문을 사용했다.
fetch를 사용하여 서버에 요청을 보낼 때 발생 할 수 있는 오류는 네트워크문제, 서버오류, 클라이언트오류 가 있다.

이러한 상황들은 서버와의 통신 중 예상치 못한 오류를 발생시킬 수 있고 이런 오류상황에 대비해 try/catch 문을 사용하는 것이 좋다고 한다.
즉 어플리케이션의 안정성을 높일 수 있다.

동기? 비동기? 이해하기

공부하던 중 자꾸 나오는 비동기에 대해 알아보자

동기
Synchronous : 동시에 발생하는

  • 순차적/직렬적으로 테스크를 수행한다.
  • 요청을 보냈다면, 응답을 받아야 다음 동작이 이루어진다.
  • 순차적으로 실행되므로, 어떤 작업이 수행중이라면 뒤의 작업은 대기한다.
  • 블로킹(작업중단)이 발생한다.

비동기
Asynchronous : 동시에 발생하지 않는

  • 병렬적으로 테스크를 수행한다.
  • 현재 작업의 종료여부와 무관하게 다음 작업을 실행한다.
  • 동기방식과는 다르게 완료 순서가 보장되지 않는다.
  • 블로킹이 발생하지 않는다.

자바스크립트는 동기 방식으로 코드를 해석한다고 한다.

fetch()함수

  • 리소스를 비동기 요청 할 수 있다.
  • 주로 API를 호출하고 응답 데이터를 받아오는 역할이다.
fetch(url)
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.log('실패했어용'));
  • url : 요청을 보낼 서버의 URL
  • 첫번째 .then : 서버의 응답을 처리, JSON형식의 응답을 파싱한다.
  • 두번째 .then : 서버로부터 받은 데이터 처리
  • catch : 요청 중 발생한 오류 처리

HTTP 메서드 지정 및 헤더 추가하기

fetch(url, {
  method: 'POST', // GET, POST, PUT, DELETE 등
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(data) // 서버에 전송할 데이터
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('실패했어용'));

특징

  • Promise 기반 : 비동기 작업처리에 .then() 과 .catch를 사용할 수 있다.
  • 최신 자바스크립트 기능과 잘 통합된다. async/await 구문과 함께 사용할 수 있다.

Promise?
자바스크립트 비동기 처리에 사용되는 객체

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패 혹은 오류가 발생한 상태

async & await

function callAsync() {
    const data = fetch('데이터url')
    
    if(data.id === 1) {
      console.log(data.name);
    }
  }

이 함수에서 콘솔창의 결과는 장담할 수 없다.
fetch는 비동기로 작동하기 때문에 콜백함수로 처리해야만 결과를 보장받을 수 있다.
이 단점을 보완하기 위해 등장한 것이 async와 await 이다.

async function callAsync() {
    const data = await fetch('데이터url');

    if(data.id === 1) {
      console.log(data.name);
    }
  }

async 이라고 비동기함수를 선언해주고, 그 작업에 await를 붙여주면 이 결과를 사용하는 작업에서 동기처럼 처리가 된다.
즉, 비동기작업이 이루어진 후에 그 결과로 작업이 수행된다.
Promise, Fetch, Async, Await에 대해 알아보자

0개의 댓글