Vue.js에 비해 pure Javascript에 가까운 React.js를 이용하다보면, 서버 개발자가 익숙하지 않은 Javascript 기초 문법이나 기반 기술들에 대해 먼저 살펴볼 필요가 생긴다. 특히, 현대적인 Javascript 기법에 익숙해져야 올바른 React.js 개발이 가능하다.
화살표를 이용한 lambda(이름없는) 함수 정의하기
() => { };
파라미터가 없는 lambda function
(item) => { };
item => { };
item이라는 파라미터 한 개 짜리 lambda function
파라미터가 1개인 경우에는 소괄호를 생략할 수 있음
(item, label) => { };
파라미터가 두 개 이상인 경우에는 소괄호를 생략할 수 없음
function add(a, b) { return a + b; }
const add = (a, b) => { return a + b; };
예전에는 function을 명시적으로 정의해서 사용했다면, 요즘은 lambda function 스타일로 정의하는 게 일반적이다.
frontend에서 가장 많이 사용하는 promise는 http client인 fetch라고 할 수 있다.
fetch의 에러 처리는 상당히 까다롭기 때문에 다음과 같이 정의해서 쓰는 게 좋다.
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => {
response.json()
.then((result) => {
if (result.status === 'success') {
// 성공인 경우의 결과 처리
} else {
// API 데이터(비즈니스 로직)와 관련된 에러 처리
}
})
.catch((error) => {
// 결과를 JSON으로 변환하는 과정과 관련된 에러 처리
});
})
.catch((error) => {
// HTTP 통신과 관련된 에러 처리
})
.finally(() => {
// 성공이든 실패든 최후에 처리해야 할 작업
});
에러가 여러 층위에서 발생할 수 있으므로, 위와 같이 총 네 군데에서 에러 핸들링을 해야 한다.
정상 응답인 경우 뒷쪽의 then에서, 정상 응답이 아닌 경우 뒷쪽의 catch 블럭에서 각각 처리된다.
(promise의 then/catch/finally 핸들러의 공통 처리 방식임)다음과 같이 재작성하는 것을 권장한다.
참고: https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
export function handleFetchErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
첫번째 then 블럭을 공통 함수로 분리해서 재사용하는 게 코드 가독성 측면에서 유리하다.
fetch(url)
.then(handleFetchErrors)
.then((response) => {
response.json()
.then((result) => {
if (result.status === 'success') {
// 성공인 경우의 결과 처리
} else {
// API 데이터(비즈니스 로직)와 관련된 에러 처리
}
})
.catch((error) => {
// 결과를 JSON으로 변환하는 과정과 관련된 에러 처리
});
})
.catch((error) => {
// HTTP 통신과 관련된 에러 처리
})
.finally(() => {
// 성공이든 실패든 최후에 처리해야 할 작업
});
import myDefault, {foo, bar} from "my-module.js";
myDefault는 'default import'라고 불리며, my-module.js에서 export default로 선언된 것이며, 어떤 이름으로 import를 해도 상관없다.
foo와 bar는 'named import'라고 불리며, 정확한 이름을 사용하지 않으면 import가 되지 않는다.
export default const myDefaultFunction = () => { return 'my default value'; };
export const foo = 3;
export const bar = 'hello world';
const smith = { "name": "Smith", "age": 16};
console.log('Smith: ' + smith);
console.log('Smith: ', smith);
console.log(`Smith: ${smith}`);
console.log(`Smith: ${JSON.stringify(smith)}`);
const obj1 = { name: '🍔', price: 30.89, group: 1, };
const obj2 = { name: '🍨', price: 20.71, group: 1, };
const obj3 = { name: '🍿', price: 10.31, group: 2, };
const obj4 = { name: '🍵', price: 5.98, group: 2, };
console.log(obj1, obj2, obj3, obj4);
console.log({obj1, obj2, obj3, obj4});
const itemList = ['hello', 'world', 'java', 'music'];
for (let item of itemList) {
if (item.length < 5) {
console.warn(item);
} else {
console.log(item);
}
}
let count = 0
console.time()
for (let i = 0; i < 1000000000; i++) {
count++
}
console.timeEnd()
const foods = [
{ name: '🍔', price: 30.89, group: 1, },
{ name: '🍨', price: 20.71, group: 1, },
{ name: '🍿', price: 10.31, group: 2, },
{ name: '🍵', price: 5.98, group: 2, },
];
console.table(foods);
const를 최대한 활용하여 불변성을 유지하는 것은 개발자가 실수하지 않도록 하여 코드 결함을 최소화하기 위해 매우 중요하다. 그러나 const로 선언하게 되면 어떤 객체의 일부만 변경해서 사용하기가 어렵고 let으로 선언하게 되면 기존 객체의 불변성이 깨지기 때문에 바람직하지 않다. 이런 경우에 객체를 복사해서 일부만 변경해서 사용하는 게 바람직한데 이걸 문법적으로 편의성을 제공하고 있다.
const obj1 = {
a: 1,
b: 2,
c: 3
};
const obj2 = {
...obj1,
d: 4
};
점 3개로 구성된 spread operator(펼치기 연산자)를 이용하면 객체의 멤버들을 펼쳐준다. 함수 호출 시에 나머지 파라미터들을 펼쳐서 받아내거나 객체 안에서 다른 객체의 멤버들을 가져다가 사용할 때 이렇게 활용한다. 일반 배열이든 객체(보통은 연관 배열)든 모두 가능하다.
위 예제처럼 이렇게 obj1의 값을 모두 취하면서 새로운 키-값을 덧붙이는 건 명확하다.
const obj3 = {
...obj1,
a: 10,
};
const obj4 = {
a: 20,
...obj1
};
obj3의 경우, 마지막에 지정한 a 키의 값 10으로 overwrite되고, obj4의 경우, obj1.a 키의 값 1로 overwrite되므로 순서에 주의할 필요가 있다. 원래 spread operator를 사용하려던 목적을 떠올린다면 당연히 obj3와 같은 형태로 사용하는 게 바람직하다.
일반적으로 promise를 재사용하는 것은 쉬운 문제가 아니다. promise가 어떤 결과를 반환하는 함수가 아니고 미래에 어떤 결과를 반환하겠다는 약속일 뿐이라서 일반적인 함수를 추상화해서 재사용하는 기법을 동일하게 적용할 수는 없다.
특히 fetch promise를 보면, 최소 then이 두 세번씩 사용되기도 하고, 중첩해서 promise가 내부 promise를 가지기도 하고 예외가 발생할 수도 있는 경우에는 문법적으로 재사용하기가 상당히 까다롭다.
promise를 반환하는 함수를 작성해서 반환된 값에 대해 then 핸들러와 catch 핸들러를 추가 작성하면 된다. 그러나 이 방법은 비슷한 promise 사용에 대해 추상화가 잘 되지 않을 뿐더러 결국 비슷한 코드를 중복해서 작성하게 되는 것에서 벗어나기 힘들다. 그러나 promise를 값처럼 주고받으면서 사용하는 방식 자체는 정석적이라고 볼 수 있다.
const getData = (url, data, headers) => {
return fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: headers
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject(response);
});
};
getData(url1, data1, headers1)
.then((data) => {
myHandler1(data);
})
.catch((error) => {
throw error;
});
getData(url2, data2, headers2)
.then((data) => {
myHandler2(data);
})
.catch((error) => {
throw error;
});
promise를 추상화해서 재사용하는 방법으로 1번 방법처럼 return된 걸 가지고 접근하는 방법은 결국 then 핸들러와 catch 핸들러를 달아줘야 해서, 코드가 verbose해지는 것을 피할 수 없다. 차라리 함수형 언어의 장점을 살려서 내 비즈니스 로직을 처리할 핸들러를 lambda 함수로 넘기는 방식이 더 적절하다.
const getData = (url, data, headers, resolve, reject, final) => {
return fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: headers
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject(response);
})
.then((data) => {
resolve(data);
})
.catch((error) => {
if (reject) {
reject(error);
}
});
.finall(() => {
if (final) {
final();
}
});
};
getData(url1, data1, headers1, myHandler1, errorHandler);
getData(url3, data3, headers3, (data) => { console.log(data); }, (error) => { console.error(error); }, () => { });
예를 들어, fetch promise 사용을 이렇게 추상화한다면 코드가 깔끔해진다.
const apiReq = (url, method, type, resolve, reject, final) => {
try {
fetch(url, {method: method})
.then(handleFetchErrors)
.then((response) => {
const promise = type === 'JSON' ? response.json() : (type === 'TEXT' ? response.text() : response.blob());
return promise;
})
.then((data) => {
if (type === 'JSON') {
if (data['status'] === 'success') {
resolve(data['result']);
} else {
reject(data['error']);
}
} else {
resolve(data);
}
})
.catch((error) => {
if (reject) {
reject(error);
}
})
.finally(() => {
if (final) {
final();
}
});
} catch (error) {
if (reject) {
reject(error);
}
}
};
export const jsonGetReq = (url, resolve, reject, final) => apiReq(url, 'GET', 'JSON', resolve, reject, final);
export const textGetReq = (url, resolve, reject, final) => apiReq(url, 'GET', 'TEXT', resolve, reject, final);
export const blobGetReq = (url, resolve, reject, final) => apiReq(url, 'GET', 'BLOB', resolve, reject, final);
const downloadUrl = '/download/dirs/' + dirName + '/files/' + encodeURIComponent(fileName);
setLoading(true);
textGetReq(downloadUrl, (result) => {
setFileContent(result);
setErrorMessage(null);
}, (error) => {
setErrorMessage(`file content load failed, ${error}`);
setFileContent(null);
}, () => {
setLoading(false);
});