4주차에는 리덕스를 새로 배웠기 때문에 피드백에 대한 내용보다는 공부한 내용을 정리한 글을 위주로 작성했었다.
5주차에는 그래도 javascript를 공부하면서 많이 봤던 비동기에 대해서 공부하는 시간이었다.
비동기는 병렬적으로 태스크를 수행한다. 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고(작업 중단을 하지 않고) 다음 태스크를 실행한다. (참고)
서버에서 데이터를 가져와서 작업을 하기 위해서는 비동기처리가 필수적이다.
async 함수 선언은 함수 내에서 await 키워드가 허용되는 async 함수를 선언한다. async 함수는 항상 Promise 값을 반환하고, async와 await 키워드는 promise-based의 동작을 깔끔한 스타일로 작성할 수 있게 한다. await 키워드를 사용하면 Promise가 처리될 때까지 기다린다.
참고
코드숨 과제를 할 때, fetch 메서드를 사용해서 서버에 있는 데이터를 가져오고 보내는 작업을 했다.
// 자주 쓰는 url은 변수에 할당하는 것이 편하다고 생각하여 변수에 할당하였다!
const URL = 'https://eatgo-customer-api.ahastudio.com';
// GET
export const fetchRegions = async () => {
const response = await fetch(`${URL}/regions`);
const data = await response.json();
return data;
};
export const fetchSelected = async (regionName, categoryId) => {
const response = await fetch(`${URL}/restaurants?region=${regionName}&category=${categoryId}`);
const data = await response.json();
return data;
};
// POST
export async function postLogin({ email, password }) {
const url = 'https://eatgo-login-api.ahastudio.com/session';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const { accessToken } = await response.json();
return accessToken;
}
export async function postReview({
accessToken,
restaurantId,
score,
description,
}) {
const url = `https://eatgo-customer-api.ahastudio.com/restaurants/${restaurantId}/reviews`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ score, description }),
});
await response.json();
};
// response.json() : 응답을 json 형태로 파싱
fetch의 기본 문법 : fetch(url, [options])
참고 사이트
REST API가 무엇인지 공부하기 위해서 코드숨에서 첨부해놓은 영상을 봤다.
https://www.youtube.com/watch?v=RP_f5dMoHFc
REST(REpresentational State Transfer)는 로이필딩이 HTTP 프로토콜을 고치게 되면서 기존에 있는 것에 대한 호환성 문제 발생을 고려하여 HTTP Object Model을 만들었다. 그리고 4년 후 이것이 바로 REST란 이름으로 발표를 하게 되고, 2년 후에는 REST로 박사 논문을 썼다고 한다.
REST API는 REST 아키텍쳐 스타일을 따르는 API이다. 아키텍쳐 스타일은 제약 조건의 집합인데,제약조건을 모두 지켜야 REST API라고 할 수 있다.
REST를 구성하는 스타일
Uniform interface의 제약조건
uniform interface의 제약조건 중 아래 2가지는 거의 지키지 못 하고 있다.
왜 API는 REST를 지키는 게 잘 안될까...?
흔한 웹 페이지 | HTTP API | |
---|---|---|
Protocol | HTTP | HTTP |
커뮤니케이션 | 사람-기계 | 기계-기계 |
Media Type | HTML | JSON |
HTML | JSON | |
---|---|---|
Hyperlink | 됨(a 태그 등) | 정의되어 있지 않음 |
Self-descriptive | 됨(HTML 명세) | 불완전 |
self-descriptive와 HATEOAS를 지키는 방법은 있다...!
self-descriptive를 지키기 위해서는 media type을 정의해주거나, Link 헤더에 profile relation으로 의미를 정의한 명세를 링크하는 것이다. HATEOAS를 지키기 위해서는 data에 다양한 방법으로 하이퍼링크를 표현하고, Link, Location 등 HTTP 헤더로 링크를 표현한다.
좀 더 자세한 내용은 이 블로그를 참고하면 된다. (영상에서 추천한 블로그가 아닌 코드숨에서 참고하라고 적어놓은 블로그이다!)
Redux toolkit을 아직 적용하지 않는 단계여서, Redux에서 비동기 처리를 해주기 위해서는 Redux thunk를 사용해야 됐다. (Redux toolkit을 사용하면 redux thunk를 따로 설치하지 않아도 지원함)
Redux thunk middleware는 action creator가 액션을 반환하는 대신에 함수를 반환한다. 그래서 특정 액션이 실행되는 것을 지연시키거나 특정한 조건이 충족될 때만 액션이 실행될 수 있도록 할 수 있다. store를 셋팅할 때 미들웨어를 잡아줄 수 있음.
// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
Redux thunk를 사용하면 dispatch와 getState를 매개변수로 받아올 수 있다.
5주차 과제를 하면서, 지역과 종류를 선택하면 그에 맞는 레스토랑 목록을 서버에서 가져와야 됐다. fetchRestaurants라는 api 호출 함수에는 regionName, categoryId를 인자로 전달받아서 사용하도록 만들어놨다.
loadRestaurants 함수에서 regionName, categoryId를 알기 위해서는 getState() 함수를 이용해서 상태를 가져올 수 있다.
// action.js
export function loadRestaurants() {
return async (dispatch, getState) => {
const {
selectedRegion: region,
selectedCategory: category,
} = getState();
if (!region || !category) {
return;
}
const restaurants = await fetchRestaurants({
regionName: region.name,
categoryId: category.id,
});
dispatch(setRestaurants(restaurants));
};
}
dispatch를 매개변수로 받아와서 dispatch 함수를 사용할 수 있다.
// action.js
export const loadRegions = () => async (dispatch) => {
const regions = await fetchRegions();
dispatch(setRegions(regions));
};
// RegionsContainer.js
export default function RegionsContainer() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadRegions());
}, []);
const { regions, selectedRegionName } = useSelector((state) => ({
regions: state.regions,
selectedRegionName: state.selectedRegionName,
}));
const handleClick = (name) => {
dispatch(selectedRegion(name));
};
return (
<Regions
regions={regions}
onClick={handleClick}
selectedRegionName={selectedRegionName}
/>
);
}
비동기 함수 테스트 코드를 작성하는 것은 어려웠다. 결국 비동기 함수 테스트 코드는 엄청난 에러 코드만 만들어내고 해결하지 못했다.
코드를 리뷰해주신 멘토님께서 옛날에 하신 테스트코드를 참고하라고 알려준 링크를 보고 공부했다...!
mockFn.mockResolvedValue(value)
: 비동기 테스트에서 비동기 함수를 mock 하는데 유용하게 쓸 수 있다.jest.fn().mockImplementation(() => Promise.resolve(value));
mockFn.mockRejectedValue(value)
: reject 되는 비동기 함수를 mock 하는데 유용하게 쓸 수 있다.jest.fn().mockImplementation(() => Promise.reject(value))
;
5주차 과제에 적용했을 때
// async-action.test.js
import { loadRegions } from './action';
import { fetchRegions } from './services/api';
jest.mock('./services/api');
describe('async-action', () => {
const dispatch = jest.fn();
// 각 테스트가 실행되기 전에 함수를 실행
beforeEach(() => {
dispatch.mockClear();
});
describe('loadRegions', () => {
context('when successfully fetch data', () => {
beforeEach(() => {
fetchRegions.mockResolvedValue(
[
{ id: 1, name: '서울' },
{ id: 2, name: '부산' },
]
);
});
it('dispatch setRegions', async () => {
await loadRegions()(dispatch);
expect(dispatch).toBeCalledWith(setInitRegions(regions));
});
});
context('when fail to fetch data', () => {
beforeEach(() => {
fetchRegions.mockRejectedValue(new Error('some error'));
});
it('dispatch setRegions', async () => {
await loadRegions()(dispatch);
expect(dispatch).not.toBeCalled();
});
});
});
describe('loadRestaurants', () => {
context('when successfully fetch data', () => {
const getState = jest.fn(() => ({
[
{ id: 1, name: '서울' },
{ id: 2, name: '부산' },
],
selectedRegion: '서울',
[
{ id: 1, name: '한식' },
{ id: 2, name: '일식' },
],
selectedCategory: '한식',
}));
beforeEach(() => {
fetchRestaurants.mockResolvedValue(restaurants);
});
it('dispatch loadCategories', async () => {
await loadRestaurants()(dispatch, getState);
expect(dispatch).toBeCalledWith(setRestaurants(restaurants));
});
});
context('without selectedRegion and selectedCategory ', () => {
const getState = jest.fn(() => ({
[
{ id: 1, name: '서울' },
{ id: 2, name: '부산' },
],
selectedRegion: '',
[
{ id: 1, name: '한식' },
{ id: 2, name: '일식' },
],
selectedCategory: '',
}));
beforeEach(() => {
fetchRestaurants.mockResolvedValue(restaurants);
});
it('dispatch loadCategories', async () => {
await loadRestaurants()(dispatch, getState);
expect(dispatch).not.toBeCalled();
});
});
context('when fail to fetch data', () => {
beforeEach(() => {
fetchRestaurants.mockRejectedValue(new Error('some error'));
});
it('dispatch loadRestaurants', async () => {
await loadRestaurants()(dispatch, getState);
expect(dispatch).not.toBeCalled();
});
});
});
})
5주차에는 피드백을 많이 받지 못했다.
useSelector.mockImplementation
을 it문에 넣어서 수정하기describe('reducer', () => {
describe('setRegions', () => {
it('지역 데이터를 저장한다', () => {})
})
})
이렇게 작성할 경우에는 "setRegions는 지역데이터를 저장한다"
이렇게 읽히기 때문에 "setRegions는 지역 목록을 변경한다"가 좀 더 적합하다고 피드백을 받았다.
const { regions, selectedRegionName } = useSelector((state) => ({
regions: state.regions,
selectedRegionName: state.selectedRegion,
}));
// 이렇게 작성하는 것보다는 용어를 통일하게 작성해주는게 좋다!
const { regions, selectedRegionName } = useSelector((state) => ({
regions: state.regions,
selectedRegion: state.selectedRegion,
}));
비동기 함수 테스트를 할 때 mockStore를 이용해서도 할 수 있다고 했다. 다음번에 할 때는 mockStore를 한번 이용해봐야 되겠다.
이때 주간회고를 작성한 것을 보면, "TDD가 익숙해졌다고 생각했는데, 내가 할 줄 아는 것은 expect와 render를 하는 것 뿐" 이라고 작성했었네...ㅎ