비동기
비동기 쉽게 이해하기
커피숍에서 커피를 주문하려고 줄을 서는 모습을 상상해 보세요. 커피숍 사정상, 커피를 주문한 먼저 온 김코딩이 주문한 커피를 받을 때까지, 줄 서 있는 박해커가 주문조차 할 수 없다고 하겠습니다. 이를 우리는 블로킹(blocking)이라고 부릅니다. 하나의 작업이 끝날 때까지, 이어지는 작업을 "막는 것"입니다.
박해커는 김코딩이 주문한 커피가 나오고 나서야 커피를 주문할 수 있습니다. 김코딩의 커피 주문 완료 시점과 박해커의 커피 주문 시작 시점이 같습니다. 이렇게 시작 시점과 완료 시점이 같은 상황을 "동기적(synchronous)이다."라고 합니다.
효율적인 커피숍 운영을 위해서 아래와 같이 커피숍 주문 과정을 변경해 봅시다.
- 커피 주문이 블로킹(blocking) 되지 않고, 언제든지 주문을 받을 수 있습니다.
- 커피가 완성되는 즉시 커피를 제공합니다.
- 김코딩의 주문 완료 시점과 박해커의 주문 시작 시점이 같을 필요가 없습니다.
Node.js를 만든 개발자도 위 대안이 합리적이라고 생각했습니다. 그래서 Node.js를 논 블로킹(non-blocking)하고 비동기적(asynchronous)으로 작동하는 런타임으로 개발하게 됩니다.
JavaScript의 비동기적 실행(Asynchronous execution)이라는 개념은 웹 개발에서 특히 유용합니다. 특히 아래 작업은 비동기적으로 작동되어야 효율적입니다.
- 백그라운드 실행, 로딩 창 등의 작업
- 인터넷에서 서버로 요청을 보내고, 응답을 기다리는 작업
- 큰 용량의 파일을 로딩하는 작업
학습 목표
- 어떤 경우에 중첩된 콜백(callback)이 발생하는지 이해할 수 있다.
- 중첩된 콜백(callback)의 단점, Promise의 장점을 이해할 수 있다.
- async/await 키워드에 대해 이해하고, 작동 원리를 이해할 수 있다.
동기와 비동기
동기(synchronous)
JavaScript의 동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것’을 의미합니다. 주문 즉시 붕어빵을 만들어 주는 노점상이 있다고 생각해 봅시다. 동기적으로 운영되는 노점상의 경우 붕어빵을 주문받은 후 주문받은 붕어빵이 다 만들어지고 난 후에야 다음 손님의 주문을 받고 붕어빵을 제작하게 됩니다. 이 경우 여러 손님의 주문을 소화하기에는 무리가 있습니다.
비동기(asynchronous)
JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미합니다. 앞선 예시로 든 노점상이 비동기적으로 운영되는 경우 여러 손님의 주문을 받으면서 붕어빵을 제작하게 되고 완성되는 대로 손님에게 붕어빵을 제공하게 됩니다. 동기적으로 운영하는 경우보다 훨씬 효율적입니다.
JavaScript의 작동원리
JavaScript는 싱글 스레드 기반으로 동작하는 언어입니다. 따라서 동기적으로 작동하게 됩니다. 그러나 방금까지 JavaScript에서도 비동기 처리가 가능하다고 배우셨는데요. 어떻게 된 걸까요? 그 이유는 JavaScript가 작동하는 환경(런타임)에서 비동기 처리를 도와주기 때문에 특별한 작업 없이 비동기 처리를 할 수 있는 것입니다.
비동기 JavaScript
JavaScript에서 비동기를 경험하게 되는 첫 번째 단계는 타이머와 관련된 API입니다. 해당 API는 브라우저에서 제공하는 Web API이며 비동기로 작동하도록 구성되어 있습니다. 아래의 설명과 코드를 직접 쳐보면서 비동기를 경험해 보세요.
타이머 관련 API
setTimeout(callback, millisecond)
일정 시간 후에 함수를 실행
- 매개변수(parameter): 실행할 콜백 함수, 콜백 함수 실행 전 기다려야 할 시간 (밀리초)
- return 값: 임의의 타이머 ID
setTimeout(function () {
console.log('1초 후 실행');
}, 1000);
// 123
clearTimeout(timerId)
setTimeout 타이머를 종료
- 매개변수(parameter): 타이머 ID
- return 값: 없음
const timer = setTimeout(function () {
console.log('10초 후 실행');
}, 10000);
clearTimeout(timer);
// setTimeout이 종료됨.
setInterval(callback, millisecond)
일정 시간의 간격을 가지고 함수를 반복적으로 실행
- 매개변수(parameter): 실행할 콜백 함수, 반복적으로 함수를 실행시키기 위한 시간 간격 (밀리초)
- return 값: 임의의 타이머 ID
setInterval(function () {
console.log('1초마다 실행');
}, 1000);
// 345
clearInterval(timerId)
setInterval 타이머를 종료
- 매개변수: 타이머 ID
- return 값: 없음
const timer = setInterval(function () {
console.log('1초마다 실행');
}, 1000);
clearInterval(timer);
// setInterval이 종료됨.
실습 예제
아래의 실습 예제는 Node.js 환경에서 작성되어 있습니다. 아직은 Node.js를 배우지 않았기 때문에 Node.js 환경에서 코드를 실행하기 위해서는 터미널에서 "node 파일명"을 입력하면 된다는 것만 기억해 주세요.
// index.js
// 1번부터 순서대로 주석을 해제하며 각 API가 동작하는 방식을 확인해보세요!
// 터미널에 `node index.js`를 입력하여 코드가 작동된 결과를 확인할 수 있습니다.
// 1. setTimeout
setTimeout(function () {
console.log('1초 후 실행');
}, 1000);
// 2. clearTimeout
// const timer = setTimeout(function () {
// console.log('10초 후 실행');
// }, 10000);
// clearTimeout(timer);
// 3. setInterval
// setInterval(function () {
// console.log('1초마다 실행');
// }, 1000);
// 4. ClearInterval
// const timer = setInterval(function () {
// console.log('1초마다 실행');
// }, 1000);
// clearInterval(timer);
[코드] JavaScript의 비동기
비동기 코드는 코드가 작성된 순서대로 작동되는 것이 아니라 동작이 완료되는 순서대로 작동하게 됩니다. 즉, 코드의 순서를 예측할 수 없습니다. 아래의 코드를 살펴보면서 이해해 보세요.
// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string) => {
setTimeout(function () {
console.log(string);
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A');
printString('B');
printString('C');
};
printAll();
console.log(`아래와 같이 비동기 코드는 순서를 보장하지 않습니다!`);
[코드] 비동기 코드
코드를 실행할 때마다 순서가 바뀌는 것을 확인을 했습니다. 프로그래밍을 하면서 개발자가 제어할 수 없는 코드를 작성하는 것은 옳지 않습니다. 개발자는 언제나 예측가능한 코드를 작성하도록 노력해야 합니다. 따라서, 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 합니다. 그 방법들은 다음 챕터부터 알아보도록 하겠습니다.
Callback
개발자는 코드 작성 시 예측 가능한 코드를 작성하도록 노력해야 한다고 했습니다. 그러기 위해서는 비동기로 작동하는 코드를 제어할 수 있는 방법에 대해 잘 알고 있어야 합니다. 여러 방법 중 하나는 Callback 함수를 활용하는 방법입니다. Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다. 즉, 비동기를 동기화할 수 있다는 의미입니다.
실습 예제
아래의 코드를 살펴보면서 어떻게 비동기를 예측가능하게 만들 수 있는지 이해해 보세요.
// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string, callback) => {
setTimeout(function () {
console.log(string);
callback();
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {});
});
});
};
printAll();
console.log(
`아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
);
[코드] Callback(7시 방향의 Fork on StackBlitz 버튼을 클릭하여 코드를 확인하세요.)
Callback Hell
Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있습니다. 아래의 코드를 살펴보면서 이해해 보세요.
실습 예제
// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string, callback) => {
setTimeout(function () {
console.log(string);
callback();
}, Math.floor(Math.random() * 100) + 1);
};
const printAll = () => {
printString('A', () => {
printString('B', () => {
printString('C', () => {
printString('D', () => {
printString('E', () => {
printString('F', () => {
printString('G', () => {
printString('H', () => {
printString('I', () => {
printString('J', () => {
printString('K', () => {
printString('L', () => {
printString('M', () => {
printString('N', () => {
printString('O', () => {
printString('P', () => {});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
};
printAll();
console.log(
`아래와 같이 Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 코드가 길어질 수록 복잡해지고 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있습니다.`
);
[코드] Callback Hell(7시 방향의 Fork on StackBlitz 버튼을 클릭하여 코드를 확인하세요.)
앞서 확인한 Callback Hell의 현상을 방지하기 위해 Promise가 사용되기 시작했습니다. 다음 챕터에서는 Promise에 대해 알아봅시다.
Promise, Async/Await
비동기로 작동하는 코드를 제어할 수 있는 다른 방법은 Promise를 활용하는 방법입니다. 또한 Callback Hell을 방지하는 역할도 수행합니다.
new Promise
Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성합니다. 또한 Promise는 비동기 처리를 수행할 콜백 함수(executor)를 인수로 전달받는데 이 콜백 함수는 resolve, reject 함수를 인수로 전달받습니다.
Promise 객체가 생성되면 executor는 자동으로 실행되고 작성했던 코드들이 작동됩니다. 코드가 정상적으로 처리가 되었다면 resolve 함수를 호출하고 에러가 발생했을 경우에는 reject 함수를 호출하면 됩니다.
let promise = new Promise((resolve, reject) => {
// 1. 정상적으로 처리되는 경우
// resolve의 인자에 값을 전달할 수도 있습니다.
resolve(value);
// 2. 에러가 발생하는 경우
// reject의 인자에 에러메세지를 전달할 수도 있습니다.
reject(error);
});
[예시] 프로미스가 에러가 발생한 경우의 프로미스 객체
Promise 객체의 내부 프로퍼티
new Promise가 반환하는 Promise 객체는 state, result 내부 프로퍼티를 갖습니다. 하지만 직접 접근할 수 없고 .then, .catch, .finally의 메서드를 사용해야 접근이 가능합니다.
State
기본 상태는 pending(대기)입니다. 비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동했다면 fulfilled(이행)로 변경이 되고, 에러가 발생했다면 rejected(거부)가 됩니다.
Result
처음은 undefined입니다. 비동기 처리를 수행할 콜백 함수(executor)가 성공적으로 작동하여 resolve(value)가 호출되면 value로, 에러가 발생하여 reject(error)가 호출되면 error로 변합니다.
then, catch, finally
Then
executor에 작성했던 코드들이 정상적으로 처리가 되었다면 resolve 함수를 호출하고 .then 메서드로 접근할 수 있습니다. 또한 .then 안에서 리턴한 값이 Promise면 Promise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아오고, Promise가 아니라면 리턴한 값을 .then 의 콜백 함수의 인자로 받아올 수 있습니다. 아래의 .then 과 Promise chaining의 예시를 살펴보면서 동작 방식을 확인해 보세요.
let promise = new Promise((resolve, reject) => {
resolve("성공");
});
promise.then(value => {
console.log(value);
// "성공"
})
Catch
executor에 작성했던 코드들이 에러가 발생했을 경우에는 reject 함수를 호출하고 .catch 메서드로 접근할 수 있습니다.
let promise = new Promise(function(resolve, reject) {
resolve("성공");
reject(new Error("에러"));
});
promise.catch(error => {
console.log(error);
// Error: 에러
})
Finally
executor에 작성했던 코드들의 정상 처리 여부와 상관없이 .finally 메서드로 접근할 수 있습니다.
let promise = new Promise(function(resolve, reject) {
resolve("성공");
reject(new Error("에러"));
});
promise
.then(value => {
console.log(value);
// "성공"
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log("성공이든 실패든 작동!");
// "성공이든 실패든 작동!"
})
Promise chaining
Promise chaining가 필요하는 경우는 비동기 작업을 순차적으로 진행해야 하는 경우입니다. Promise chaining이 가능한 이유는 .then, .catch, .finally 의 메서드들은 Promise를 리턴하기 때문입니다. 따라서 .then을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 처리하면 됩니다.
let promise = new Promise(function(resolve, reject) {
resolve('성공');
...
});
promise
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.catch((error) => {
console.log(error);
return '실패';
})
.finally(() => {
console.log('성공이든 실패든 작동!');
});
실습 예제
Promise로 작성된 아래의 코드를 살펴보면서 이해해 보세요.
// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
console.log(string);
}, Math.floor(Math.random() * 100) + 1);
});
};
const printAll = () => {
printString('A')
.then(() => {
return printString('B');
})
.then(() => {
return printString('C');
});
};
printAll();
console.log(
`아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있습니다!`
);
[코드] Promise
// 터미널에 'node index.js'을 입력하여 .then, .catch, .finally의 작동박식을 이해해보세요!
let promise = new Promise(function (resolve, reject) {
resolve('성공');
// reject("실패");
});
promise
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.then((value) => {
console.log(value);
return '성공';
})
.catch((error) => {
console.log(error);
return '실패';
})
.finally(() => {
console.log('성공이든 실패든 작동!');
});
[코드] .then, .catch, .finally, Promise chaining
Promise.all()
const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));
Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용합니다. 인자로는 배열을 받습니다. 해당 배열에 있는 모든 Promise에서 executor 내 작성했던 코드들이 정상적으로 처리가 되었다면 결과를 배열에 저장해 새로운 Promise를 반환해 줍니다.
앞서 배운 Promise chaining을 사용했을 경우는 코드들이 순차적으로 동작되기 때문에 총 6초의 시간이 걸리게 됩니다. 또한, 같은 코드가 중복되는 현상도 발생하게 됩니다.
// 기존
const result = [];
promiseOne()
.then(value => {
result.push(value);
return promiseTwo();
})
.then(value => {
result.push(value);
return promiseThree();
})
.then(value => {
result.push(value);
console.log(result);
// ['1초', '2초', '3초']
})
이러한 문제들을 Promise.all()을 통해 해결할 수 있습니다. Promise.all()은 비동기 작업들을 동시에 처리합니다. 따라서 3초 안에 모든 작업이 종료됩니다. 또한 Promise chaining로 작성한 코드보다 간결해진 것을 확인할 수 있습니다.
// promise.all
Promise.all([promiseOne(), promiseTwo(), promiseThree()])
.then((value) => console.log(value))
// ['1초', '2초', '3초']
.catch((err) => console.log(err));
추가적으로 Promise.all()은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 나머지 Promise의 state와 상관없이 즉시 종료됩니다. 아래의 예시와 같이 1초 후에 에러가 발생하고 Error: 에러1이 반환된 후로는 더 이상 작동하지 않고 종료됩니다.
Promise.all([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러1'))), 1000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러2'))), 2000),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('에러3'))), 3000),
])
.then((value) => console.log(value))
.catch((err) => console.log(err));
// Error: 에러1
실습 예제
Promise.all()로 작성된 아래의 코드를 살펴보면서 이해해 보세요.
// 터미널에 'node index.js'을 입력하여 Promise.all()의 작동방식과 에러 발생시 동작방식을 이해해보세요!
// 또한 Promise chaining과의 차이점도 확인해보세요.
const promiseOne = () =>
new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () =>
new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () =>
new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));
// 1. Promise chaining
// const result = [];
// promiseOne()
// .then((value) => {
// result.push(value);
// return promiseTwo();
// })
// .then((value) => {
// result.push(value);
// return promiseThree();
// })
// .then((value) => {
// result.push(value);
// console.log(result);
// });
// 2. Promise.all()
// Promise.all([promiseOne(), promiseTwo(), promiseThree()])
// .then((value) => console.log(value))
// .catch((err) => console.log(err));
// 2-1. Promise.all()의 에러 발생시 동작방식
// Promise.all([
// new Promise(
// (resolve, reject) => setTimeout(() => reject(new Error('에러1'))),
// 1000
// ),
// new Promise(
// (resolve, reject) => setTimeout(() => reject(new Error('에러2'))),
// 2000
// ),
// new Promise(
// (resolve, reject) => setTimeout(() => reject(new Error('에러3'))),
// 3000
// ),
// ])
// .then((value) => console.log(value))
// .catch((err) => console.log(err));
[코드] Promise.all()
Promise Hell
Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있습니다. 아래의 코드를 살펴보면서 이해해 보세요.
실습 예제
// 터미널에 `node index.js`를 입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(string);
}, Math.floor(Math.random() * 100) + 1);
});
};
const printAll = () => {
printString('A').then((value) => {
console.log(value);
printString('B').then((value) => {
console.log(value);
printString('C').then((value) => {
console.log(value);
printString('D').then((value) => {
console.log(value);
printString('E').then((value) => {
console.log(value);
printString('F').then((value) => {
console.log(value);
printString('G').then((value) => {
console.log(value);
printString('H').then((value) => {
console.log(value);
printString('I').then((value) => {
console.log(value);
printString('J').then((value) => {
console.log(value);
printString('K').then((value) => {
console.log(value);
printString('L').then((value) => {
console.log(value);
printString('M').then((value) => {
console.log(value);
printString('N').then((value) => {
console.log(value);
printString('O').then((value) => {
console.log(value);
printString('P').then((value) => {
console.log(value);
});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
};
printAll();
console.log(
`아래와 같이 Promise를 통해 비동기 코드의 순서를 제어할 수 있지만 Callback 함수와 같이 코드가 길어질수록 복잡해지고 가독성이 낮아지는 Promise Hell이 발생하는 단점이 있습니다.`
);
[코드] Promise Hell
Async/Await
JavaScript는 ES8에서 async/await키워드를 제공하였습니다. 이를 통해 복잡한 Promise 코드를 간결하게 작성할 수 있게 되었습니다. 사용법은 간단합니다. 함수 앞에 async 키워드를 사용하고 async 함수 내에서만 await 키워드를 사용하면 됩니다. 이렇게 작성된 코드는 await 키워드가 작성된 코드가 동작하고 나서야 다음 순서의 코드가 동작하게 됩니다.
// 함수 선언식
async function funcDeclarations() {
await 작성하고자 하는 코드
...
}
// 함수 표현식
const funcExpression = async function () {
await 작성하고자 하는 코드
...
}
// 화살표 함수
const ArrowFunc = async () => {
await 작성하고자 하는 코드
...
}
아래의 코드를 살펴보면서 async/await를 이해해 보세요.
실습 예제
// 터미널에 `node index.js`입력하여 비동기 코드가 작동하는 순서를 확인해보세요.
const printString = (string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
console.log(string);
}, Math.floor(Math.random() * 100) + 1);
});
};
const printAll = async () => {
await printString('A');
await printString('B');
await printString('C');
};
printAll();
console.log(
`Async/Await을 통해 Promise를 간결한 코드로 작성할 수 있게 되었습니다.`
);
[코드] Async/Await
비동기 네트워크 요청
비동기 요청의 가장 대표적인 사례는 단연 네트워크 요청입니다. 네트워크를 통해 이루어지는 요청은 그 형태가 다양합니다. 그중에서 URL로 요청하는 경우가 가장 흔합니다. URL로 요청하는 것을 가능하게 해 주는 API가 바로 fetch API입니다.
학습 목표
- fetch에 대해 이해할 수 있다.
- fetch를 이용하여 데이터를 불러올 수 있다.
fetch를 이용한 네트워크 요청
다음은 흔히 볼 수 있는 포털 사이트입니다.
[그림] 포털 사이트 예시
이 사이트는 시시각각 변하는 정보와, 늘 고정적인 정보가 따로 분리되어 있는 것을 확인할 수 있습니다. 이 중에서 최신 뉴스나 날씨/미세먼지 정보가 바로 동적으로 데이터를 받아야 하는 정보입니다.
이럴 때 많은 웹사이트에서는 해당 정보만 업데이트하기 위해 요청 API를 이용합니다. 그중 대표적인 fetch API를 이용해 해당 정보를 원격 URL로부터 불러오는 경우를 설명합니다. 다음은 원격 URL로부터 정보를 받아와서 특정 DOM 엘리먼트를 업데이트하는 콘셉트를 도식화한 그림입니다.
[그림] URL에 요청을 보내고, 필요한 정보를 받아와야 합니다.
fetch API는 위와 같이, 특정 URL로부터 정보를 받아오는 역할을 합니다. 이 과정이 비동기로 이루어지기 때문에, 경우에 따라 다소 시간이 걸릴 수 있습니다. 이렇게 시간이 소요되는 작업을 요구할 경우에는 blocking이 발생하면 안 되므로, 특정 DOM에 정보가 표시될 때까지 로딩 창을 대신 띄우는 경우도 있습니다.
fetch API
크롬 브라우저의 새 탭을 연 후, 개발자 도구의 콘솔에 다음과 같이 입력해 보세요.
let url =
"https://koreanjson.com/posts/1";
fetch(url)
.then((response) => response.json())
.then((json) => console.log(json))
.catch((error) => console.log(error));
React
React Intro
웹, 데스크탑, 모바일 등 다양한 플랫폼에서 활용할 수 있는 멋진 프론트엔드 라이브러리, React를 학습하게 되신 것을 환영합니다. 앞선 유닛에서는 페이지 단위로 프론트엔드 개발하는 방법에 대해 배웠다면, 이제는 컴포넌트라는 단위로 나누어 생각하고 개발하는 방법을 배웁니다. 아래 학습 목표를 기준 삼아 학습하시기 바랍니다.
Before You Learn
다음 항목에 대해 잘 알고 있는지 스스로 점검해 보세요. 스스로 생각하기에 보충이 필요하다면 아래의 키워드를 복습하는 것을 추천합니다.
- HTML / CSS 기초
- 자바스크립트 기초
- 함수형 프로그래밍과 고차 함수 개념에 대한 이해
- 배열 내장 메서드 기초
- ES6 문법에 대한 이해
학습 목표
- React의 3가지 특징에 대해서 이해하고, 설명할 수 있다.
- JSX가 왜 명시적인지 이해하고, 바르게 작성할 수 있다.
- React 컴포넌트(React Component)의 필요성에 대해서 이해하고, 설명할 수 있다.
- create-react-app 으로 간단한 개발용 React 앱을 만들 수 있다.
React Intro
JSX
리액트 기초 개념을 통해 리액트가 무엇인지 알게 되었습니다. 이번 챕터에서는 자바스크립트의 확장 문법 JSX와 리액트의 핵심 Component에 대해 학습합니다.
학습 목표
- React의 3가지 특징에 대해서 이해하고, 설명할 수 있다.
- JSX가 왜 명시적인지 이해하고, 바르게 작성할 수 있다.
- React 컴포넌트(React Component)의 필요성에 대해서 이해하고, 설명할 수 있다.
JSX란?
JSX 활용
map을 이용한 반복
이번 실습에서는 React에서 여러 데이터를 렌더링 할 때 사용하는 map 메서드에 대해서 알아보도록 하겠습니다. 빠른 이해를 위해김코딩의 React TIL 블로그 개발을 예시로 들겠습니다.
김코딩의 블로그 포스트가 두 개밖에 없다면 아래 코드로 충분합니다. 이렇게 직접 모든 데이터를 코드에 작성하는 것을 하드코딩(hard coding)이라고 부릅니다.
const posts = [
{ id: 1, title: "Hello World", content: "Welcome to learning React!" },
{ id: 2, title: "Installation", content: "You can install React via npm." },
];
function Blog() {
return (
<div>
<div>
<h3>{posts[0].title}</h3>
<p>{posts[0].content}</p>
</div>
<div>
<h3>{posts[1].title}</h3>
<p>{posts[1].content}</p>
</div>
</div>
);
}
하지만 김코딩의 블로그 포스트가 100개인 경우를 상상해 봅시다. 김코딩은 부지런한 수강생이기 때문에 하루에 글을 하나 이상 작성합니다. 이런 상황에서는 블로그 포스팅이 늘어날 때마다 매일 코드를 변경해야만 합니다. 데이터가 변경될 때마다, 알아서 렌더링할 수는 없을까요? React에서는 이런 문제를 해결하기 위해서 배열 메서드 map을 활용합니다.
const posts = [
{ id : 1, title : 'Hello World', content : 'Welcome to learning React!' },
{ id : 2, title : 'Installation', content : 'You can install React via npm.' },
{ id : 3, title : 'reusable component', content : 'render easy with reusable component.' },
// ...
{ id : 100, title : 'I just got hired!', content : 'OMG!' },
];
function Blog() {
return (
<div>
<div>
<h3>{posts[0].title}</h3>
<p>{posts[0].content}</p>
</div>
<div>
<h3>{posts[1].title}</h3>
<p>{posts[1].content}</p>
</div>
{// ...}
<div>
<h3>{posts[99].title}</h3>
<p>{posts[99].content}</p>
</div>
{// ... 98 * 4 more lines !!}
</div>
);
}
map을 이용한 반복
내장 고차 함수 map에서 배웠던 배열 메서드 map의 특성을 다시 떠올려봅시다.
- 배열의 각 요소를
- 특정 논리(함수)에 의해
- 다른 요소로 지정(map)합니다.
현재 posts의 요소는 블로그 포스트의 id, title, content로 나눌 수 있습니다. 이 정보를 브라우저에서 React로 보여주려면 JSX를 활용해서 이 데이터를 적절히 넣어야 합니다. 단순한 문자열에 불과했던 posts의 요소를 HTML 엘리먼트로 이 정보의 구조를 잘 짜 놓은 모습으로 매핑하면 되겠습니다. 이것을 의사코드로 작성해 봅시다.
배열의 각 요소(post)를 특정 논리(postToElement 함수)에 의해 다른 요소로 지정(map) 합니다.
React에서는 위 의사코드를 아래와 같이 적을 수 있습니다. 반복적으로 작성해야 하는 부분만 골라서 배열의 요소로 넣으면 React가 이를 인지하고 모든 요소를 렌더링합니다. 편리하죠? 앞으로 배우실 컴포넌트를 재사용성 있게 만들면 단순 반복되는 코드를 간결하게 작성할 수 있습니다.
function Blog() {
const postToElement = (post) => (
<div>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
const blogs = posts.map(postToElement);
return <div className="post-wrapper">{blogs}</div>;
}
return 문 안에서 map 메서드를 사용할 수는 없을까요? 사용할 수 있습니다. JSX를 사용하면 중괄호 안에 모든 표현식을 포함할 수 있기 때문에 map 메서드의 결과를 return문 안에 인라인으로 처리할 수 있습니다. 코드 가독성을 위해 변수로 추출할지 아니면 인라인에 넣을지는 개발자가 판단해야 할 몫입니다.
key 속성
React에서 map메서드 사용 시, key 속성을 넣지 않으면 아래와 같이 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시됩니다. key 속성의 위치는 map 메서드 내부에 있는 엘리먼트 즉, 첫 엘리먼트에 넣어주세요.
key 속성값이 반드시 id가 되어야 하나요? id가 존재하지 않으면 어떻게 해야 하나요? key 속성값은 가능하면 데이터에서 제공하는 id를 할당해야 합니다. key 속성값은 id와 마찬가지로 변하지 않고, 예상 가능하며, 유일해야 하기 때문입니다. 정 고유한 id가 없는 경우에만 배열 인덱스를 넣어서 해결할 수 있습니다. 배열 인덱스는 최후의 수단(as a last resort)으로만 사용합니다. 리액트 공식문서의 key에서 추가로 공부하세요.
function Blog() {
// postToElement라는 함수로 나누지 않고 아래와 같이 써도 무방합니다.
const blogs = posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
));
return <div className="post-wrapper">{blogs}</div>;
}
Component
Create React App
create-react-app를 사용한다면 단 하나의 명령 만으로 자신만의 리액트 프로젝트를 시작할 수 있습니다. 개발자는 빌드에 신경 쓰지 않고, 곧바로 코드 작업에 집중할 수 있습니다. 이번 챕터에서 Create React App이 무엇인지 학습하고, 과제 - React Twittler Intro에서 JSX 문법을 직접 사용해 봅니다.
학습 목표
- Create React App 소개를 보고, Create React App 이 무엇인지 대략적으로 이해할 수 있다.
- npx create-react-app 으로 새로운 리액트 프로젝트를 시작할 수 있다.
- create-react-app 으로 간단한 개발용 React 앱을 실행할 수 있다.
- Create React App으로 만들어진 리액트 프로젝트 구성을 대략적으로 이해할 수 있다.
Create React App 시작
React SPA
React SPA 개요
웹페이지에서는 페이지를 유저에게 보여줄 때 즉, 페이지를 로딩할 때마다 서버에 미리 준비되어 있는 페이지를 전달 받아와서 렌더링을 했습니다. 하지만, 규모가 커질수록 사용자와의 상호 작용이 많아지고 그에 따라, 속도 저하 등의 문제가 발생하게 됩니다. React에서는 이러한 문제를 해결하기 위해 SPA를 사용하고 있습니다. 이번 챕터에서는 React SPA의 등장 배경과 기본 개념에 대해 학습하고 컴포넌트를 어떻게 활용할 수 있을지 고민해 봅니다.
학습 목표
- SPA(Single-Page Application) 개념을 이해하고 설명할 수 있다.
- SPA의 장, 단점에 대해 이해하고 설명할 수 있다.
- 와이어프레임을 보고 어느 부분을 컴포넌트로 구분할지 스스로 정할 수 있다.
SPA(Single Page Application)
Wireframe
React Router
React Router 개요
React SPA에서는 경로에 따라 다른 뷰를 보여줄 수 있습니다. 라우팅에 따라 다른 뷰를 보여주기 위해서 React에서는 React Router라는 라이브러리를 많이 사용합니다. React Router를 어떻게 사용하는지 학습하고, 튜토리얼을 따라가며 실습해 봅니다. 그리고 과제 - React Twittler SPA에 직접 적용해 보며, 연습합니다.
학습 목표
- React에서 npm으로 React Router를 설치(npm install react-router-dom@^6.3.0)하고 이용할 수 있다.
- React Router를 이용하여 SPA를 구현할 수 있다.
- 라우팅 구조를 짤 수 있어야 하고, 이에 필요한 기초 문법들을 사용할 수 있어야 한다.
React Router