[항해99] 미니 프로젝트 후기 feat. 백엔드와 첫 협업

김헤일리·2022년 12월 23일
0

TIL

목록 보기
15/46

3주 동안 리액트에 대해서 공부하고 처음으로 백엔드와 협업을 진행했다.
짧은 시간동안 리액트에 대해서 숙지할 수 있다고 생각도 안했지만, 이렇게까지 어려울 줄을 상상도 못 했다 😭

API 명세서를 같이 작성했음에도 어떻게 이해하고 사용하는지 몰라서 초반에 굉장히 혼란스러웠다.
심지어 서로 개발을 마치고 맞춰보는 단계에서부터 난리가 나서 혼란스러움이 이루 말할 수 없었다.

그래도 좋은 팀원들 덕분에 확실하게 성장할 수 있었다.


1. API 명세서를 이해하고 요청 보내자!

❗️ API 명세서를 잘 읽어야 하는 이유

  • 프로젝트 시작 때 백엔드 분 중 한분이 조장이었고, (잘 몰랐지만 굉장한 실력자였다...!) 다 같이 모여서 api 명세서를 작성했었다.

    • 이때 api 명세서가 뭔지도 잘 몰랐고, 뭔지는 알아도 이걸 어떻게 사용하는지에 대해선 전-혀 생각을 하지 않았었다.
  • 처음에 json 서버를 이용해서 기능 구현하는 것만 익숙해서 나도 모르게 자연스럽게 이미 이전 과제에서 했던 코드를 똑같이 따라하고 있었다. 강의를 보면서 따라하는거에 익숙해서 그랬던것 같다.

  • 당연히... 데이터가 하나도 연결이 안됐고, 요청도 응답도 받을 수 없었다.

    • 서버에서 응답으로 보내주는 데이터에서 필요한 부분을 뽑아서 사용해야한다.
    • 이때 요청을 제대로 해야만 서버는 응답을 내려주기 때문에 서버가 받을 수 있는 형식의 데이터를 제대로 전달해주는 것이 매우매우! 중요하다.

❗️ Axios instance, interceptor 사용법

  • 기존에 진행했던 프로젝트는 axios를 사용할 때 baseURL을 전부 매번 할당해서 사용했었다.
  • 그래서 서버 주소가 바뀔 때 매번 모든 axios 요청의 baseURL을 변경해줘야 했었다.
  • 매번 하나하나 바꾸긴 힘들기 때문에 axios intance를 사용하기로 했다.

    1. Axios Instance

    • 일단 [core]라는 폴더에 axios.js 파일을 만들고, 해당 파일에 axios instance 코드를 추가했다.
export const authInstance = axios.create({
  // 1. authInstance라는 변수에 axios instance를 만들어서 할당한다.
  baseURL: "http:서버주소.com",
  // 2. baseURL을 서버 주소로 설정한다.
  headers: {
  // 3. axios는 기본적으로 js 데이터를 json 형식으로 변경한다.
  // 3-1. 만약 serialized JSON Object를 전달할 경우, “application/x-www-form-urlencoded” 형식으로 파일을 전달한다.
    Accept: "application/json",
    "Content-Type": "application/json",
    // 3-2. json 형식으로 확실하게 전달하기 위해서 headers에 어떤 content type을 명시해야 한다고 한다.
  },
});
  • 이렇게 instance를 설정하면 앞으로 axios를 사용할 때 매번 baseURL을 포함해서 적을 필요가 없다고 한다.
    • authInstance.get('/baseURL 다음에 붙일 api 주소') 형식으로 사용할 수 있다!
    • 반복적이지 않는 코드가 많아야 좋은 코드라고 했던가... 앞으로도 더 효율적으로 코딩할 수 있도록 여러 방면에서 생각해봐야겠다!

2. Axios Interceptor

  • axios instance가 기본 설정이라면, axios interceptor는 서버로 보내는 요청과 서버로 부터 받는 요청을 중간에 가로채서 필요한 일을 한 다음 데이터를 다시 전달한다고 한다.

  • 요청 인터셉터

authInstance.interceptors.request.use((config) => {
  // 1. axios instance를 이용해서 요청을 보낼 때, 중간에 요청을 가로채서 서버로 요청을 보내기 전에 함수가 실행된다. 
  if (config.headers === undefined) return;
  // 2. 요청 config.header가 undefined면 아무 일도 실행되지 않는다.
  const token = localStorage.getItem("id");
  // 3. 로컬스토리지에 'id'라는 값으로 저장한 내용을 'token'이라는 변수에 담고,
  config.headers["Authorization"] = `${token}`;
  // 3-1. 로그인하면서 얻은 token 값을 그대로 header에 넣어서 요청을 보낸다.
  return config;
});
  • 에러 핸들링 (응답 인터셉터)
const errorInterceptor = (error) => {
// 1. 서버에서 응답을 줄 때 해당 응답을 중간에 가로채는 응답 인터셉터도 활용할 수 있다.
// 1-1. 해당 인터셉터는 서버에서 내려준 response가 error response일 때 응답을 가로챈다.
  if (error.response.status === 401) {
  // 2. 만약 서버에서 보낸 응답 에러가 401에러일 경우,
    window.location.replace("/login");
    // 3. 로그인 페이지로 이동시키고
    alert("로그인 기간이 만료되었습니다. 다시 로그인 해주세요.");
    // 3-1. 로그인 기간이 만료되었다는 alert 메세지를 송출한다.
    localStorage.clear();
    // 4. 그리고 로컬 스토리지의 내용도 싹 다 비워준다!
  }
  return Promise.reject(error);
};

authInstance.interceptors.response.use(errorInterceptor);
// 5. 그리고 응답 인터셉터 중 errorInterceptor라는 인터셉터를 활용하겠다는 코드를 추가한다.
  • 로그인이 포함된 서비스라면, 검증된 (토큰이 있는) 사용자만 접속할 수 있어야 하기 때문에 요청 시 무조건 토큰을 함께 보내야 한다.
  • 토큰은 한번 발급받고 끝이 아니라 만료 기간이 있기 때문에 만료된 상태에서 서비스에 접근할 시 오류가 발생한다.
    • 이때 발생한 오류에 대해 사용자에게 안내해야 하기 때문에 응답 인터셉터를 이용한 에러 핸들링이 중요하다!


2. useState 이해하고 이용하기

  • 서버에 보내는 요청과 응답에 대해서 배운 게 가장 컸지만, 기존에 배웠던 내용도 더 보완할 수 있었다.
  • 지금까지 useState 훅이 정확히 뭔지 모르고 사용했었는데, 이번 프로젝트에서 생각보다 다양하게 state라는 개념을 사용할 수 있었다.

✏️ help text 사용법

  • 로그인 시 지금까진 유효성 검사, 로그인 성공 여부를 전부 alert 메세지를 이용해서 안내했었다.
  • 매번 메세지가 뜨는게 불편하기 때문에 help text가 더 낫다고 생각은 했지만, 어떻게 구현하는지 몰라서 못 했었다 😭
  • 이번 프로젝트에서 useState 훅을 이용해서 드디어 help text를 만들어볼 수 있었다!
**** 대표 예시: 비밀번호 ****

// 인풋 값 가져오기
const [password, setPassword] = useState("");
// 1. 인풋의 value를 지정할 state 선언
	
// 오류 메세지 출력
const [passMsg, setPassMsg] = useState("");
// 2. 오류 메세지가 생길 경우 해당 메세지를 담을 state 선언

// 유효성 검사
const [isPass, setIsPass] = useState(false);
// 3. 사용자가 설정한 비밀번호가 유효하지 않을 경우 state는 false, 유효할 경우 true로 설정한다.

// input onChange 함수
const onChangePassword = (e) => {
// 4. password를 쓰는 input의 값이 바뀌는 것을 감지하는 함수
    const currentPassword = e.target.value;
  	// 4-1. currentPassword라는 변수에 현재 이벤트가 발생하는 target의 value를 담는다.
    setPassword(currentPassword);
  	// 4-2. 변경되는 값을 setPassword에 담아서 password의 state가 currentPassword로 될 수 있도록 설정한다.
    const passRegExp = /^[a-zA-Z0-9!@#$%^&*]{8,15}$/;
  	// 4-3. 비밀번호 유효성 검사를 위해 passRegExp라는 변수에 백엔드와 협의한 내용으로 regular expression을 넣는다. 
  	// 4-4. 대소문자와 숫자, 특수문자를 포함한 8-15자리의 비밀번호만 받는다.
    if (!passRegExp.test(currentPassword)) {
    // 4-5. 특정값이 정규표현식의 조건에 부합하는지 확인하는 js의 메소드는 regExp.test(string)이다.
    // 4-6. 민약 currentPassword의 값이 passRegExp의 조건을 충족하지 못 하면,
      setPassMsg("8-15자의 대소문자, 숫자, 특수문자(!@#$%^&*)로 입력해주세요.");
      // 4-7. setPassMsg에 경고문구 (help text)를 추가한다.
      setIsPass(false);
      // 4-8. 유효하지 않은 비밀번호기 때문에 유효 여부를 확인하는 setIsPass의 state는 false로 설정한다.
    } else {
      // 4-9. 만약 currentPassword의 값이 passRegExp의 조건에 부합한다면,
      setIsPass(true);
      // 4-10. 유효 여부를 확인하는 setIsPass의 state를 true로 변경한다.
    }
  };

// return문 안에 있는 내용
<div>
  <Input
	onChange={onChangePassword}
	// 4-11. 사용자가 비밀번호를 입력할 인풋필드에 onChangePassword 함수를 심어서 사용자가 작성하는 값을 인식한다.
    type="password"
    name="password"
    label="비밀번호를 입력하세요."
    width={"250px"}
   	/>
</div>
<p 
  style={{
  	ontSize: "11px",
    color: "red",
  }}
>
 {passMsg}
// 2-1. 오류 메세지가 생길 경우 이 곳에 메세지가 출력된다.
</p>
  • 이렇게 해서 처음으로 help text를 만들어볼 수 있었다.
  • 비록 인풋필드의 갯수가 많아지면 관리할 state가 계속 늘어나서.. 효율적이지 않은 방식이라는 피드백을 들었지만, 처음으로 새로운 것을 만들어보고 useState훅에 대해 더 이해할 수 있어서 좋은 시간이었다.
  • 다음엔 전부 state로 관리하지 않고 useForm을 이용해서 더 효율적인 방식으로 유효성 검사를 해봐야겠다!

✏️ state에 boolean 넣기

  • useState에 boolean값도 이번에 처음 넣어서 사용해봤는데, 생각보다 굉장히 사용할 방식이 무궁무진했었다.
  • 회원가입 시 유효성 검사에도 boolean으로 유효한 값 여부를 설정했었지만, if문을 사용하는 것처럼 state가 true일 때 화면에 출력하는 내용을 변경할 수도 있었다.
  • 이번 프로젝트에선 [댓글 보기 버튼]을 클릭 했을 때 화면에 댓글 리스트가 생기게 만들어봤다.
// 버튼 클릭 여부를 확인하기 위한 state
const [display, setDisplay] = useState(false);

  return (
    <div>
      <SmallButton
        onClick={() => {
          setDisplay(!display);
        }}
        // 1. 버튼을 클릭 했을 때 display의 state를 반대로 변경한다.
        // 1-1. !display라고 설정할 경우, true일 땐 false로, false일 땐 true로 값을 변경시킨닫.
      >
        {display && "Hide"}
		// 2. 만약 display의 값이 true일 경우, 버튼에 "Hide"라고 표시된다.
        {!display &&
        // 3. 만약 display의 값이 false일 경우, 
          (commentList.length === 0
           // 3-1. 서버로부터 받아온 commentList의 길이가 0일 경우,
            ? "Replies"
           	// 3-2. 버튼의 내용이 "Replies"라고 표시되고,
            : `View ${commentList.length} more replies`)}
			// 3-3. 0이 아닐 경우 "view commentList.length more replies"라는 문구가 표시된다.
      </SmallButton>
      <div>
        {display &&
        // 4. 그리고 display가 true일 경우,
          commentList.map((item) => (
          // 4-1. commentList 배열을 map 돌려서 모든 코멘트가 1개씩 표시되도록 한다.
            <div key={item.id}>
            // 4-2. map을 돌릴 땐 하나의 item이 각자 고유의 key를 가져야하기 때문에 코멘트 아이디를 key로 설정하고, 
                <div>{item.nickname}</div>
                <div>{item.content}</div>
                <div>{item.createdAt}</div>
				// 4-3. 각 코멘트에 있는 nickname, content, createdAt을 전부 표시한다.
            </div>
          ))}
      </div>
    </div>
  );
}
  • 간단한 방법으로 조금 더 interactive한 기능을 추가할 수 있어서 좋았다.
  • boolean으로 state를 설정해서 더 다양한 조건을 통해 여러 기능을 추가할 수 있을 것 같다!
  • 다음 프로젝트에서 유용하게 써먹을 수 있을 것 같다.


팀원과의 협업, 백엔드와 연결하는 방법, api 명세서 이해하고 요청 주고 받기 등등 이번 프로젝트는 새로운 시도를 많이 했다.

리액트만 공부하다가 처음으로 프론트 개발자처럼 프로젝트를 진행해본건데, 정말... 매우 힘들었다 🤯
요청을 주고 받는다는 느낌이 뭔지 이제 좀 감이 잡히고 전달받은 데이터를 저장하는 방식 등등..
아직 갈 길이 매우 멀지만, 이제 조금 감을 잡은 것 같아서 힘들면서도 배울 점이 많은 프로젝트였다!

출처:

profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄

0개의 댓글