[Day 38] JS(예외 처리) & React(JSX 소개 + 요소 렌더링 + 컴포넌트와 Props + State와 라이프사이클 + 이벤트 제어하기 + 조건부 렌더링)
예외 처리 (Exception Handling)
코드 실행 중에 예기치 못한 에러가 발생했을 때, 이로부터 코드의 실행 흐름을 복구할 수 있는 기능
동기식 코드에서의 예외 처리
- try {} catch (e) {}
- 에러가 났을 때 원상복구를 시도할 코드를
try
블록 내부에 작성하면, 에러가 발생했을 때 코드의 실행 흐름이try
블록에서catch
블록으로 옮겨간다. catch
블록 안에서는 에러에 대한 정보를 담고 있는 객체(e)를 사용할 수 있다.
- 에러가 났을 때 원상복구를 시도할 코드를
- finally {}
finally
블록에 있는 코드는try
블록 안에서의 에러 발생 여부와 관계 없이 무조건 실행된다.- 에러가 안 났을 때 실행 순서:
try
-finally
- 에러가 났을 때:
try
- 에러 발생 -catch
-finally
직접 에러 발생시키기
Error
생성자와throw
구문을 이용해서 프로그래머가 직접 에러를 발생시킬 수 있다.- 간혹 에러의 종류를 구분해야 하거나, 에러 객체에 기능을 추가해야 할 필요가 있을 때가 있다.
- 이럴 때
Error
를 상속받는 클래스를 만들어서,throw
구문에서 이 클래스를 대신 사용할 수 있다.
- 이럴 때
비동기식 코드에서의 예외 처리
비동기 콜백
-
비동기식으로 작동하는 콜백의 내부에서 발생한 에러는, 콜백 바깥에 있는
try
블록으로는 잡아낼 수 없다.try { setTimeout(() => { throw new Error('에러!'); }); } catch (e) { console.error(e); }
-
JS 엔진은 에러가 발생하는 순간 호출 스택을 되감는 과정을 거친다. 이 과정 중에
try
블록을 만나야 코드의 실행 흐름을 원상복구 시킬 수 있다.try { setTimeout(() => { throw new Error('에러!'); }); } catch (e) { console.error(e); }
-
위의 코드 처럼
try
블록을 비동기 콜백 내부에 작성해줘야 catch할 수 있다. -
addEventListener()
같은 콜백들도 비동기 콜백처럼 콜백 내부에서 try ~ catch 구문을 상용해줘야 한다.
Promise
Promise 객체는 세 가지 상태를 가질 수 있다.
pending
- Promise 객체에 결과값이 채워지지 않은 상태fulfilled (or resolved)
- Promise 객체에 결과값이 채워진 상태rejected
- Promise 객체에 결과값을 채우려고 시도하다가 에러가 난 상태
Promise 객체가 rejected
상태가 되면, 이 Promise에 대해서는 then
메소드에 두 번째 인수로 넘겨준 콜백이 실행된다. (이 콜백에는 에러 객체를 인수로 넘겨준다.)
p.then(even => {
return '짝수입니다.';
}, e => {
return e.message;
}).then(alert);
위의 코드처럼 then
메소드 내부에서 두 번째 인수로 에러 콜백을 넘기는 것도 가능하지만, catch
메소드를 통해 에러 처리 콜백을 지정해줄 수도 있다.
p.then(even => {
return '짝수입니다.';
}).catch(e => {
return e.message;
}).then(alert);
만약 then
메소드 체인 도중 에러가 발생하면 이후 코드 진행을 건너뛰면서 처음 만나는 에러 처리 콜백으로 실행 흐름이 넘어간다.
Promise.resolve()
.then(() => {
throw new Error('catch 메소드를 통해 예외 처리를 할 수 있습니다.');
})
.then(() => {
console.log('이 코드는 실행되지 않습니다.');
})
.catch(e => {
return e.message;
})
.then(console.log);
비동기 함수
비동기 함수 내부에서는, rejected
상태가 된 Promise 객체를 await
할 경우에만 동기식 예외 처리 방식과 동일하게 try… catch… finally 구문으로 처리할 수 있다.
async function func() {
try {
const res = await fetch('https://nonexistent-domain.nowhere');
} catch (e) {
console.log(e.message);
}
}
func(); // 출력 결과: Failed to fetch
(주의!) Promise 객체에 대해 await
구문을 사용하지 않는 경우, 에러가 발생해도 catch
블록으로 코드의 실행 흐름이 넘어가지 않는다..
async function func() {
try {
fetch('https://nonexistent-domain.nowhere');
} catch (e) {
console.log(e.message);
}
}
func(); // 아무것도 출력되지 않습니다.
React Basic
JSX 소개
JSX는 자바스크립트의 확장 문법이다. 리액트를 JSX와 함께 사용하면, UI가 어떻게 보일지를 서술해줄 수 있다. JSX는 템플릿 언어처럼 보이지만, 오직 자바스크립트만을 기반으로 동작하고 있고, 모두 자바스크립트로 변환될 수 있다.
const element = <h1>Hello, world!</h1>;
JSX에 표현식 포함하기
JSX 안에 자바스크립트 표현식을 중괄호로 묶어서 포함시킬 수 있다.
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
JSX 또한 표현식이다.
컴파일이 끝나면, JSX 표현식은 자바스크립트 객체로 평가된다.
즉, if문이나 for문 안에서 JSX를 사용할 수도 있고, 변수에 할당하거나 매개변수로 전달하거나 함수에서 반환할 수 있다는 것이다.
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX Attribute 정의
따옴표(““)를 사용하면 문자열 리터럴을 정의할 수 있다.
const element = <div tabIndex="0"></div>;
중괄호({})를 사용하면 자바스크립트 표현식을 포함시킬 수 있다.
const element = <img src={user.avatarUrl}></img>;
(주의!) JSX Attribute는 HTML의 Attribute와 이름이 다른 경우가 있고, JSX Attribute의 이름을 정의할 때에는 camelCase 를 사용한다.
- class -> className
- tabindex -> tabIndex
- label for -> label htmlFor
JSX 자식 정의
만약 태그가 비어있다면, XML 처럼 />
를 이용해 태그를 닫아줘야 한다.
const element = <img src={user.avatarUrl} />;
JSX 태그는 자식 요소를 가질 수 있다.
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 인젝션 공격 예방
JSX에는 cross-site-scripting(XSS) 공격을 예방하는 기능이 내장되어 있다.
사용자가 입력한 내용을 JSX 내에 포함시켜도 안전하다.
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
JSX 객체 표현
Babel은 JSX를 React.createElement()
호출로 컴파일한다.
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 위 코드는 아래처럼 컴파일 된다.
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// 위 코드를 실행하면 아래와 같은 자바스크립트 객체가 생성된다.
// 이 객체를 'react element'라고 부른다.
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};
요소 렌더링 (Element Rendering)
요소(Element)는 React 앱에서 가장 작은 단위의 재료이다.
요소는 화면에 표시하고자 하는 내용을 서술한다.
const element = <h1>Hello, world</h1>;
(중요!) React Element는 그냥 순수한 객체이다. 또한 DOM Element와는 다르게 생성 비용이 저렴하다(적게든다).
React DOM 라이브러리는 브라우저의 DOM 갱신 작업을 관장하며, React 요소와 DOM이 일치하도록 만든다.
DOM에서 요소 렌더링 하기
React로 구축한 App은 보통 하나의 Root DOM Node를 가진다.
React Element를 Root DOM Node에 렌더링하고 싶다면 ReactDOM.render()
에 react element와 root dom node를 넘겨주면 된다.
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
렌더링된 요소 업데이트
React Element는 순수한 객체이기 때문에 변경 불가능하지는 않지만, 값을 변경하고 싶다면 새로운 값을 만드는 기법(Immutability)을 사용해야 한다.
- ** 불변성 (Immutablity) : 변경 불가. 값을 변경하고 싶을 때는 값을 새로 만든다.
UI를 갱신하기 위해서는 새로운 요소를 만들어서 ReactDOM.render()로 전달해야 한다.
- ** 실무에서, 대부분의 React App은 ReactDOM.render()를 한 번만 호출한다. (setState로 다시 그린다.)
React는 꼭 필요한 부분만 갱신한다.
React DOM은 요소 및 그 자식을 이전 버전과 비교하고, DOM을 원하는 상태로 만드는 데 필요한 DOM 업데이트만 적용한다.
- 개발자도구에서 확인해보면 DOM에서 변경된 부분만 보라색으로 표시되는 것을 알 수 있다.
컴포넌트와 props
컴포넌트를 통해 UI를 독립적이고 재사용 가능한 부분으로 분리하고, 각 부분을 독립적으로 생각할 수 있다.
함수형 컴포넌트
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
클래스 컴포넌트
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
컴포넌트 렌더링
React Element는 사용자 정의 컴포넌트를 나타낼 수도 있다.
const element = <Welcome name="Sara" />;
React가 사용자 정의 컴포넌트를 가리키는 요소를 처리할 때는, JSX Attribute를 하나의 객체를 통해 컴포넌트로 전달한다. 이 객체를 props
라고 부른다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
컴포넌트 조립하기
컴포넌트의 출력에서 다른 컴포넌트를 가져와서 사용할 수 있다.
// Welcome을 여러번 렌더링하는 App 컴포넌트
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
일반적으로, 새롭게 작성되는 React 앱은 단일 App
컴포넌트를 최상위에 둔다. (관례)
기존 앱에 React를 도입하는 경우, 버튼 같은 작은 컴포넌트부터 덩치를 키워나간다.
컴포넌트 추출
컴포넌트는 재사용이 용이하도록 더 작은 단위로, 더 일반적인 단위로 수정하는 것이 좋다.
Props는 읽기전용(read-only)
컴포넌트를 함수나 클래스 중 어떤 걸로 선언했던 간에, 자기 자신의 props를 수정할 수 없다.
-
순수 함수 : 동일한 입력에 대해 항상 동일한 결과를 반환한다.
function sum(a, b) { return a + b; }
(React의 엄격한 규칙!)
** 모든 React 컴포넌트는 props에 대해서는 순수 함수처럼 동작해야 한다.
- 같은 props가 입력되었을 때에는 항상!!! 같은 element가 반환되어야 한다.
- render() 메소드는 개발자가 컨트롤 할 수 없는, 자동으로 실행되는 부분이기 때문에 매번 다른 결과가 출력되면 안된다.
- state는 React 컴포넌트가 이 규칙을 어기지 않고 유저 액션, 네트워크 응답, 기타 등등에 대한 응답으로 시간 경과에 따라 출력을 변경할 수 있게 한다.
State와 라이프 사이클
컴포넌트의 생애주기마다 코드를 실행하는 것을 라이프사이클
이라고 한다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
클래스에 라이프사이클 메서드 추가하기
컴포넌트가 제거될 때 사용중이던 자원을 돌려놓는 작업은 아주 중요한 일이다.
- Clock이 DOM에 최초로 렌더링 될 때 타이머를 설정. (
mounting
) - Clock이 DOM에서 삭제되었을 때 타이머를 해제 (
unmounting
)
컴포넌트가 마운트(mount) 되거나 언마운트(unmount) 되는 시점에 코드를 실행하기 위해, 컴포넌트 클래스에 특별한 메소드를 선언할 수 있다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
/* 이런 메서드 들을 '라이프사이클 훅' 이라고 부른다.*/
componentDidMount() {
// 컴포넌트가 DOM에 mount(rendering)된 직후 실행될 코드
}
/* 이런 메서드 들을 '라이프사이클 훅' 이라고 부른다.*/
componentWillUnmount() {
// 컴포넌트가 unmount 되기 직전 실행될 코드
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
this.setState()
를 사용해서 컴포넌트의 local state에 대한 업데이트를 예약해줘야 한다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
State를 바르게 사용하기
-
State를 직접 수정하지 마세요.
// Wrong this.state.comment = 'Hello'; // Correct this.setState({comment: 'Hello'});
this.state
를 할당할 수 있는 유일한 장소는 생성자 함수 내부이다. -
State 업데이트는 비동기일 수 있다.
React는 성능을 위해 여러
setState()
호출을 한 번의 작업으로 묶어서 처리하는 경우가 있다. =>this.props
및this.state
가 비동기로 업데이트 될 수 있기 때문에 다음 state를 계산할 때 이전 state 값을 신뢰해서는 안된다.// Wrong this.setState({ counter: this.state.counter + this.props.increment, });
이러한 문제를 해결하기 위해 객체가 아닌 함수를 인자로 받는 setState()를 사용할 수 있다.
// Correct this.setState((prevState, props) => ({ counter: prevState.counter + props.increment })); // 화살표 함수 또는 일반 함수로도 가능하다. // Correct this.setState(function(prevState, props) { return { counter: prevState.counter + props.increment }; });
-
State 업데이트는 병합된다.
setState()
를 호출할 때, React는 넘겨받은 객체를 현재 state에 병합한다.Object.assign(병합할 빈 객체, ...병합될 객체들)
과 같이 ‘얕은 병합’ 로직으로 작동한다.- 얕은 병합(얕은 복사) - 해당 요소 내부의 최상위 요소만 병합(복사)된다.
- 중첩된 내부의 요소들은 병합(복사)되지 않는다.
(중요!)
setState()
는 얕은 병합을 수행하기 때문에, state에 중첩된 형태의 자료 구조를 쓰는 것은 좋지 않다.// state는 여러 독립적인 변수를 갖는다. constructor(props) { super(props); this.state = { posts: [], comments: [] }; } // 개별 setState()를 호출해서 아이템을 각각 업데이트 할 수 있다. componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
이벤트 제어하기
- React 이벤트는 소문자 대신 camelCase를 사용한다.
- JSX에 문자열 대신 함수를 전달한다.
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
React에서는 비동기 함수를 이벤트 리스너에 그냥 등록하는 것은 위험하다!!!
- 이벤트
e
는 합성 이벤트이다. - React 에서는 이벤트
e
를 한번만 쓰지 않고, 여러번 돌려 쓰기 때문에 비동기로 이벤트가 등록될 경우 이벤트 리스너가 변경되는 문제가 발생할 수 있다. - 이러한 문제를 방지하기 위해 이벤트 돌려쓰는 것을 방지하는
e.persist()
메소드를 활용해야 한다.
JSX 콜백에서 this의 의미
이벤트 리스너를 등록할 때 전달하는 메소드를 바인드하지 않고 이벤트 리스너에 전달하면, this
는 함수가 실제로 호출될 때 undefined
로 취급된다.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 콜백에서 `this`가 제대로 동작하게 만들려면 아래 바인딩을 꼭 해주어야 합니다.
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
이벤트 리스너를 등록할 때 bind
를 호출하지 않고 등록하기 위해서는 아래와 같은 방법을 사용할 수 있다.
-
이벤트 리스너의 콜백에서 화살표 함수를 사용한다.
class LoggingButton extends React.Component { handleClick() { console.log('this is:', this); } render() { // This syntax ensures `this` is bound within handleClick // 여기에서의 this는 render() 함수 내부에서 호출되고 있기 때문에 하나의 값으로 고정된다. return ( <button onClick={(e) => this.handleClick(e)}> Click me </button> ); } }
조건부 렌더링
-
if
를 사용하는 예시function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <UserGreeting />; } return <GuestGreeting />; } ReactDOM.render( // Try changing to isLoggedIn={true}: <Greeting isLoggedIn={false} />, document.getElementById('root') );
-
&& 논리 연산자를 사용해 if를 인라인으로 넣기
React는
true
,false
,null
을 그려달라고 하면 아무것도 안그려준다.function Mailbox(props) { const unreadMessages = props.unreadMessages; return ( <div> <h1>Hello!</h1> {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); } const messages = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render( <Mailbox unreadMessages={messages} />, document.getElementById('root') );
-
조건부 연산자를 사용해 if-else 인라인으로 넣기
// 삼항 연산자를 활용해 넣기. render() { const isLoggedIn = this.state.isLoggedIn; return ( <div> The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in. </div> ); }
-
컴포넌트가 렌더링 되지 못하도록 방지
어떤 컴포넌트에 의해 렌더링된 컴포넌트를 숨기고 싶은 경우, 요소 대신
null
을 반환하면 된다.- 컴포넌트의
render
메소드에서null
을 반환한다고 해서, 컴포넌트의 라이프사이클 메소드 호출 과정에 영향을 미치지는 않는다. (componentWillUpdate
와componentDidUpdate
는 호출된다.)
function WarningBanner(props) { if (!props.warn) { return null; } return ( <div className="warning"> Warning! </div> ); } class Page extends React.Component { constructor(props) { super(props); this.state = {showWarning: true} this.handleToggleClick = this.handleToggleClick.bind(this); } handleToggleClick() { this.setState(prevState => ({ showWarning: !prevState.showWarning })); } render() { return ( <div> <WarningBanner warn={this.state.showWarning} /> <button onClick={this.handleToggleClick}> {this.state.showWarning ? 'Hide' : 'Show'} </button> </div> ); } } ReactDOM.render( <Page />, document.getElementById('root') );
- 컴포넌트의
댓글남기기