와장창 프로젝트 경험기 #4

sham·2021년 9월 8일
0

Bobs 개발일지

목록 보기
4/6
post-thumbnail

현재 상황


남은 과제

  • fetch로 서버와 통신하기
  • 컴포넌트 최적화 (리렌더링 방지)

구현

Bann

42 API와 통신하기

// Bann.js
const url = 'https://api.intra.42.fr/oauth/token';
  const query =
    '?' +
    'grant_type=client_credentials' +
    '&' +
    'client_id=' +
    '착한 사람 눈에만 보이는 아이디' +
    '&' +
    'client_secret=' +
    '착한 사람 눈에만 보이는 비밀번호' +
    '&' +
    'redirect_uri=' +
    'http://localhost:3000/mypage' +
    '&' +
    'scope=public';
  useEffect(async () => {
    const fetchToken = await fetch(url + query, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'X-Mobile': 'false',
        'response-Type': 'text',
      },
    });
    const response = await fetchToken.json();
    setToken(response.access_token);
    console.log(response.access_token);
  }, []);

useEffect를 사용해서 함수형 컴포넌트(hooks)에서 컴포넌트의 생명주기를 적용시켜줄 수 있다. 한 번 발급받은 액세스 토큰을 계속 사용해 줄 것이므로 한 번만 실행되게 하자. 두번째 인자를 비워놓으면 맨 처음 렌더링 될 때 한 번만 실행된다. 클래스 컴포넌트의 componentDidMount처럼 사용할 수 있게 된다. 위의 url 뒤에 쿼리 스트링을 붙여서 POST 요청을 보내면 응답으로 토큰이 포함된 객체가 온다. async / await을 이용해서 then 없이도 동기적으로 작동되게 할 것이다.

// Bann.js
  const getUser = async id => {
    try {
      const fetchId = await fetch(`https://api.intra.42.fr/v2/users/${id}`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          Authorization: bearer + token,
          'response-Type': 'text',
        },
      });
      if (fetchId.ok === false) {
        return 'error'; // throw로 catch로 넘겨줄 수도 있지만 여기서는 리턴받는 값이 if문에서 걸러지게 error를 전송.
      }
      const result = await fetchId.json();
      return result.login;
    } catch (e) {
      // 사용자 문제가 아닌 자바스크립트 내부에서 에러가 발생했을 때 catch 사용?
      return e;
    }
  };

getUser는 Modal창이 나온 상태에서 차단하기로 결정했을 때 콜백함수로 호출된다. 42 API에서 GET으로 인자로 받은 id를 찾을 것이다. 헤더의 Authorization 속성의 값으로 "bearer " + 액세스 토큰을 넣어주고 GET 요청을 보낼 수 있다. 값이 올 때까지 기다려야 하므로 비동기적인 코드를 동기로 바꾸기 위해 역시 async / await을 사용했다. 존재하지 않는다면 'error'를 리턴한다. try, catch로 리턴한 값은 promise처럼 then, catch로 잡는다.

존재하는 사용자라면 응답으로 사용자에 대한 정보를 Response 객체로 리턴한다. 그대로 사용할 수 없으니 json코드로 변환하는 작업이 필요하다.

존재하지 않는 값을 보내면 error를 리턴하게끔 했다. HTTP 상태 코드도 404를 응답한다.


이슈

Objects are not valid as a React child (found: Error: error). If you meant to render a collection of children, use an array instead.



try, catch 문에서 fetch로 GET 요청을 보낸 id가 존재하지 않을 경우 throw를 보내서 catch를 보내주려고 했지만 다음과 같은 에러가 발생했다. 원인은 찾지 못하고 then으로 리턴을 받아서 검증하게 했다.

42 API

https://velog.io/@sham/Fetch%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-42-API%EC%99%80-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0
충격과 공포!

이 부분에서 가장 많은 시간을 쏟은 만큼 42 API로 통신하는 것은 큰 난관이었다. 다행히 정리된 블로그가 있었지만 fetch로 통신하는 사람이 없어서 삽질을 적잖이 해야만 했다. 그만큼 성공했을 때의 기쁨도 더 커서 정상적으로 GET POST 되었을 때는 무심코 소리를 지를 뻔 했다ㅎ


중요 개념

제어 흐름과 에러 처리

https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Control_flow_and_error_handling

const tryCatch = async (num) => {
  try {
    if (0 < num) {
      console.log("시작입니다");
      return "양수입니다.";
    } else throw new Error("catch로 보내지겠지?");
  } catch (e) {
    return e;
  } finally {
    console.log("끝입니다.");
  }
};

tryCatch(1).then(console.log).catch(console.log);
tryCatch(-1).then(console.log).catch(console.log);

try...catch로 제어 흐름을 관리할 수 있다. try 블록 안의 내용을 실행하다 throw 구문이 실행되면 예외로 간주되어 즉시 제어가 catch 구문으로 넘어가게 된다. 아무런 예외도 발생하지 않았다면 catch 블록은 건너뛰어진다. throw로 Error 뿐만 아니라 다른 자료형도 넘길 수 있다.
finally 블록은 try 블록이던 catch 블록이던 끝나면 무조건 실행된다.

throw Error 와 throw new Error의 차이점
https://stackoverflow.com/questions/9156176/what-is-the-difference-between-throw-new-error-and-throw-someobject

async, await / try, catch

https://www.daleseo.com/js-async-async-await/
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Async_await

function fetchAuthorName(postId) {
  return fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
    .then((response) => response.json())
    .then((post) => post.userId)
    .then((userId) => {
      return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
        .then((response) => response.json())
        .then((user) => user.name);
    });
}

fetchAuthorName(1).then((name) => console.log("name:", name));

promise를 사용한 비동기 처리를 하면 promise가 사용된 함수의 호출할 때 뒤에 .then, .catch를 붙이는 것으로 resolve, reject를 리턴받게 된다. 이때 .then().then().then() 처럼 바인딩을 하는 방식을 많이 사용하는데 가독성이 떨어질 뿐더러 어느 부분인지도 헷갈려 디버깅에 어려움을 겪을 수 있다.

이를 개선한 것이 ES7의 async, await 문법이다. 이것을 사용하면 비동기 코드를 동기 코드처럼 보이게 작성할 수 있게 된다.

async function fetchAuthorName(postId) {
  const postResponse = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`
  );
  const post = await postResponse.json();
  const userId = post.userId;

  try {
    const userResponse = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    const user = await userResponse.json();
    return user.name;
  } catch (err) {
    console.log("Faile to fetch user:", err);
    return "Unknown";
  }
}

fetchAuthorName(1).then((name) => console.log("name:", name));

async 선언을 해준 함수 안에서 비동기 작업이 일어나게 되는 코드 앞에 await을 붙이면 해당 작업이 끝날 때까지 코드가 진행되지 않는다. 비동기 코드가 동기적으로 작동하게 되는 것이다.

제어 흐름, 에러 처리도 try, catch 문을 사용해서 처리할 수 있다.

fetch

https://velog.io/@sham/Fetch%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

생명주기와 useEffect

클래스형 컴포넌트에서는 컴포넌트가 만들어졌을 때, state가 변경되었을 때, 소멸할 때 같은 상태에 따른 제어를 componentDidMount, componentDidUpdate, componentWillUnmount 등등을 통해 해줄 수 있다. hooks를 사용한 함수형 컴포넌트에서는 useEffect를 이용해서 생명주기를 흉내내줄 수 있다.

const interval = useRef(null);
useEffect(() => { //componentDidMount, componentDidUpdate 역할
		console.log("다시 실행");
		interval.current = setInterval(() => {console.log("반복")}, 300);
		return () => { // componentWillUnmount 역할
			console.log("종료");
			clearInterval(interval.current);
		}		
	}, [imgCoord]);

첫번째 인자에는 생명주기가 변함에 따라 실행될 코드, 두번째 인자에는 그 생명주기를 변경하게 되는 기준이 될 값이 들어간다. 클래스의 생명주기와 useEffect의 특징을 비교하면 다음과 같다.

  • 무조건 시작하자마자 한 번은 실행한다. - componentDidMount
  • 두 번째 인자에 들어간 값이 변경할 때마다 첫 번째 인자의 코드가 실행된다. - componentDidUpdate
  • 컴포넌트가 소멸될 때 리턴 하고자하는 코드가 실행된다. - componentWillUnmount

두 번째 인자를 비워놓으면 한 번만 실행되고 다시는 실행되지 않는다. (componentDidMount) 리턴을 한다면 컴포넌트가 소멸할 때 실행된다. (componentDidMount)
두 번째 인자에 두 개 이상의 값을 넣을 수도 있다. 몇 개가 들어가든지 개 중 하나라도 값이 변하면 useEffect는 실행된다.

const [itemA, setItemA] = useState(0);
const [itemB, setItemB] = useState(0);
const [itemC, setItemC] = useState(0);

useEffect(() => {
	setItemA(1);
	setItemB(1);
  console.log("A나 B 중 하나는 변경됨.");
	}		
}, [itemA, itemB]);
useEffect(() => {
	setItemB(1);
	setItemC(prevC => prevC + 1);
	console.log("B 나 C 중 하나는 변경됨.");
	}		
}, [itemB, itemC]);

useEffect를 여러개 써서 state가 변경될 때마다 useEffect를 실행시켜 줄 수도 있는데, useEffect가 클래스형 컴포넌트의 생명주기와 일대일 대응하는 것처럼 보여도, 엄연히 다르다는 것을 보여주는 부분이다.

위 코드처럼 useEffect 내부에서 setState로 값을 변경했을 때 변하는 state가 두 번째 인자 안에 있다면 그것 역시 useEffect가 다시 실행되는 조건이 충족되어 진다.
위 코드처럼 절대값을 넣어주었다면 한 번 더 실행되고 말겠지만 setItemC(prevC => prevC + 1);처럼 코드를 짤 경우 조건을 계속 충족하게 되어 useEffect가 무한으로 실행된다.

OAuth 2.0과 액세스 토큰

https://velog.io/@sham/Fetch%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-42-API%EC%99%80-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B8%B0

42 API를 정리하면서 같이 정리했지만 링크만 달랑 달아놓기는 뭣해 간단하게 짚고 넘어가겠다. OAuth 2.0는 인증 프로토콜이다. 요즘 나오는 게임이나 서비스 어플들 중 다음, 네이버, 구글 등 플랫폼 계정을 연동하게 되는 경우가 있을 것이다. 이 때 그 고객이 해당 플랫폼 유저임을 증명해야 하는데, 안전하게 검증할 수 있는 방법이 OAuth이다. 이 검증을 위해 해당 플랫폼의 API에 사용자에 대한 정보를 요청하게 되는데 요청하는 우리가 누구인지, 믿을 만한 사람인지를 액세스 토큰을 통해 증명하는 것이다.


느낀 점

어찌어찌 구현에는 성공했지만 막상 정리하려고 하니 쉽지가 않았다. 머리로 알고 있는 것과 글로 써서 정리하는 것은 엄연히 다른 것임을 다시금 느꼈다. 막막하게만 느껴졌던 42 API와의 소통을 구현해내니 굉장히 뿌듯했다. 조금씩 조금씩 하다보니 점점 그럴 듯한 모양새를 갖추게 되었다. 첫 프로젝트인 만큼, 확실하게 해내자.

profile
씨앗 개발자

0개의 댓글