[Day 39] JS(모듈) & React(리스트와 키, 폼, 합성vs상속) & 실습(TodoList)
예외 처리 (Exception Handling)
모듈 (Module)
Javascript 코드를 여러 파일과 폴더에 나눠서 작성하고, 서로가 서로를 효율적으로 불러올 수 있도록 해주는 시스템
모듈 시스템은 ES2015에서 추가되었으며, 최근에 (Chrome 61, FF 60, SF 10.1) 브라우저에서 사용할 수 있게 되었다.
아래 코드 처럼 script
태그에 type="module"
어트리뷰트를 추가해주면, 이 파일은 모듈로서 동작한다.
<script type="module" src="index.mjs"></script>
다만, 구형 브라우저에서는 모듈 기능을 지원하지 않기 때문에 Webpack, Parcel 같은 모듈 번들러를 통해 변환과정을 통해 일반적인 JS 파일로서 불러오는 방법을 사용해야 한다.
모듈이란?
모듈도 JS 코드를 담고 있는 파일이지만, 일반적인 JS 파일과는 다른점이 있다.
import
혹은export
구문을 사용할 수 있다.- 별다른 처리를 안해줘도 자동으로 엄격 모드(strict mode)로 동작한다.
- 모듈의 가장 바깥쪽에서 선언된 이름은 전역 스코프가 아니라, 모듈 스코프 에서 선언된다.
모듈 스코프
모듈 스코프에서 선언된 이름은 (export 해주지 않는다면) 해당 모듈 내부에서만 접근할 수 있다.
그렇기 때문에 여러 모듈의 가장 바깥쪽에서 같은 이름으로 변수, 함수, 클래스를 선언하더라도 서로 다른 스코프에서 선언되기 때문에 이름의 충돌이 생길 일이 없다.
export & import
모듈 스코프에서 정의된 이름은 export
구문을 통해 다른 파일에서 사용할 수 있다. 이를 ‘이름이 지정된 export’ 라는 뜻에서 named export
라고 부른다.
// variables.js
const foo = 'bar';
const spam = 'eggs';
// foo, spam을 다른 파일에서 사용할 수 있도록 export 해주었습니다.
export { foo, spam };
다른 파일에서 import
구문을 통해 가져와서 사용할 수 있다.
// main.js
// variables 모듈에 선언된 이름을 사용하기 위해 import 해주었습니다.
import { foo, spam } from './variables.js';
console.log(foo);
console.log(spam);
함수나 클래스도 export
를 통해 여러 모듈에서 재사용할 수 있다.
// functions.js
function add(x, y) {
return x + y;
}
class Person {
// ...
}
export { add, Person };
다른 모듈에 있는 이름을 사용하려면, 반드시 해당 모듈에서 이름을 export
해주어야 한다.
선언과 동시에 export 하기
이름을 선언하는 구문 앞에 export
를 붙여주면, 선언과 export
를 한꺼번에 할 수 있다.
// common.js
export const foo = 'bar';
export const spam = 'eggs';
export function add(x, y) {
return x + y;
}
export class Person {
// ...
}
default export
export default
구문을 통해 모듈을 대표하는 하나의 ‘값’을 지정하고, 그 값을 다른 모듈에서 불러와서 사용할 수 있다. (하나의 모듈에 오직 하나의 값만 default로 지정할 수 있다.)
// foo.js
export default 'bar';
import
구문에서 이름을 적어주는 부분에 중괄호를 생략하면, 모듈의 default export를 가져온다.
// main.js
import foo from './foo.js' // 아무 이름이나 사용해서 값을 가져올 수 있다.
console.log(foo); // bar
export default
뒤에는 임의의 표현식이 올 수 있다. (값으로 변환될 수 있는 거라면 뭐든지 올 수 있다.)
// add.js
// 익명 함수를 값으로 내보낸다.
export default function (x, y) {
return x + y;
}
// main.js
import add from './add.js';
console.log(add(1, 2)); // 3
// 표현식 자리에 익명 클래스를 값으로 내보낼 수도 있다.
export default class {
render() {
}
}
import
구문에서 default export와 일반적인 export를 동시에 가져올 수 있다.
// `React`라는 이름의 default export와,
// Component, Fragment라는 일반적인 export를 동시에 가져오기
import React, { Component, Fragment } from 'react';
다른 이름으로 export & import 하기
이름의 뒤에 as
를 붙여서 다른 이름이 사용되게 할 수 있다.
const foo = 'bar';
export { foo as FOO }; // FOO 라는 이름으로 export 됩니다.
import { Component as Comp } from 'react'; // Comp라는 이름으로 import 됩니다.
모듈 사용 시 주의할 점
import
구문과 export
구문은 모듈 간 의존 관계를 나타내는 것일 뿐, 코드를 실행시키라는 명령이 아니다.
같은 모듈을 여러 다른 모듈에서 불러와도, 모듈 내부의 코드는 단 한번만 실행된다.
import
구문과 export
구문은 모듈의 가장 바깥쪽 스코프에서만 사용할 수 있다.
모듈을 어떤 환경에서 실행하느냐에 따라서 구체적인 로딩 순서나 동작방식이 조금씩 달라질 수도 있다.
React Basic
리스트와 키
컴포넌트 여러 개를 렌더링 하기
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
기본적인 목록 컴포넌트
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
위 코드를 실행하면 목록의 각 항목에 키를 넣어야 한다는 경고가 표시된다.
‘키(key)’는 요소 목록을 만들 때 포함해야하는 특수한 문자열 속성이다.
// 위 코드의 li element에 key attribute를 넣어주면 경고가 안뜬다!
<li key={number.toString()}>
{number}
</li>
키 (Key)
키를 지정해주면 어떤 아이템이 바뀌었는지, 추가되었는지, 삭제되었는지를 React에게 ‘정확하게’ 알려줄 수 있다.
키는 각 항목을 고유(유일)하게 식별할 수 있어야 한다. 대부분의 경우 데이터의 ID를 키로 사용한다.
만약, 데이터의 ID 같은 유일한 식별자가 없다면, 배열 인덱스를 키로 사용할 수도 있다.
(주의!) 항목 간 순서가 바뀔 수 있는 경우에는 배열 인덱스를 키로 사용하면 성능이 저하되거나 컴포넌트의 state 관련 문제가 발생할 수 있다.
키로 컴포넌트 추출하기
키는 바로 바깥쪽의 배열에 대해서만 의미를 가진다.
function ListItem(props) {
// 여기서 키를 넣어주는 것이 아니라...
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 배열을 순회하는 함수 안에서 키를 넣어주어야 한다!
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
키는 형제 중에서 고유한 값이여야 한다.
key
와 ref
는 컴포넌트로 전달되지 않는다. 즉, 값으로 사용할 수 없다는 것이다.
만약 컴포넌트에서 key와 동일한 값이 필요하다면 명시적으로 다른 이름의 prop으로 만들어서 전달해야 한다.
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
** 같은 컴포넌트여도 키가 바뀌게 되면 아예 새로운 것으로 판단되기 때문에, 관련된 모든 인스턴스와 상태가 날라가게 되기 때문에 사실상 컴포넌트가 리셋된다. => 컴포넌트의 상태 초기화를 위해 키를 바꾸는 기법으로 사용 가능하다.
클래스 컴포넌트와 관련된 중요한 사실
우리가 클래스 컴포넌트를 만들면, 클래스의 인스턴스가 자동으로 생성된다. 그리고 클래스 컴포넌트 안에 정의된 this는 클래스의 인스턴스를 가리키게 된다. 즉, this에 정의된 속성과 상태들은 인스턴스에 붙는다고 볼 수 있다.
화면을 다시 그리면서 화면에 표시되지 않는 컴포넌트들은 그 인스턴스와 인스턴스에 속한 속성, 상태가 전부 날라가게 된다.
즉, 화면에 표시되지 않는 컴포넌트의 상태는 살아있을 수 없다.
** 함수는 인스턴스를 가질 수 없기 때문에 함수형 컴포넌트가 상태를 갖는 것이 불가능하다고 생각되지만 => 함수형 컴포넌트라는 것의 인스턴스에 대해 상태를 붙이는 작업이 가능하도록 개발중이다 (Facebook의 React 팀에서)
폼 (Form)
DOM API에서 폼을 다루는 형식과 React에서 폼을 다루는 형식은 꽤 많이 다르다.
HTML 폼 요소는 그 자체가 내부 상태를 가지고 있다.
HTML 폼 요소의 상태 저장소와 React 컴포넌트의 상태 저장소는 다른 공간이다. => 상황에 따라 어떤 상태 저장소를 ‘진리의 유일한 원천’ 으로 사용할 지 정해야 한다.
제어되는 컴포넌트 (Controlled Components)
HTML 폼 요소의 상태를 무력화시키고, React 의 상태를 기반으로 그리라는 것만 그리게끔 만든다. (상태를 끌어올림으로써 HTML 폼의 상태를 React에서 제어할 수 있게끔 만든다.)
React의 상태를 ‘진리의 유일한 원천’ 으로 만들어 HTML 폼 상태와 React 상태를 결합한다. 이렇게 하면 상태의 제어권은 React의 컴포넌트에게 넘어간다.
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
value 어트리뷰트가 폼 요소(input 요소)에 설정되었기 때문에 표시되는 값은 항상 this.state.value
가 된다.
즉, HTML 폼의 상태는 무시되고 React의 상태가 진리의 유일한 원천이 된다.
키 입력이 일어날 때마다 handleChange가 동작하고 React의 상태가 업데이트 되므로, 화면을 다시 그리면서 입력한 값이 input 창에 표시된다.
이처럼, 제어되는 컴포넌트를 사용하면 모든 상태 변경과 연관되는 핸들러 함수가 생긴다. 이를 통해 입력 요소를 수정하거나 검증하는 일이 쉬워진다. (사용자의 입력에 즉각 반응할 수 있다.)
textarea 태그
input 요소와 마찬가지의 방법으로 작성할 수 있다.
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'Please write an essay about your favorite DOM element.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('An essay was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Essay:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
select 태그
select attribute에 value를 넣어서 강제로 선택되어 있는 것처럼 보이게 할 수 있다.
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite La Croix flavor:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
제어되는 컴포넌트에 대한 대안책
제어되지 않는 컴포넌트로 폼을 만드는 것도 충분히 가능하다.
제어되지 않는 컴포넌트를 만들려면 DOM 객체를 React 세계로 가져와야 한다. (ref 속성을 이용해서 가져온다.)
합성(composition) vs 상속(inheritance)
다른 프로그래밍 언어에서 상속을 통해 기능을 재사용하는 경우가 많은데, React에서는 상속을 하지말고 컴포넌트를 합성(조합) 해서 기능을 재사용하는 문제를 해결하는 것이 좋다.
다른 컴포넌트를 담기
컴포넌트에 어떤 자식이 들어올 지 미리 알수 없는 경우에 children
이라는 특별한 prop을 통해 자식 요소를 출력에 그대로 전달하는 방법을 사용할 수 있다.
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children} <!-- 리액트가 특별히 관리하는 children이라는 prop을 사용해서 내부 컴포넌트를 받아서 그린다. -->
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<!-- 여기서부터 {props.children}에 담긴다. -->
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
<!-- 여기까지 담긴다 -->
</FancyBorder>
);
}
Try it on Cod
특수화 (Specialization)
원래있던 컴포넌트를 ‘특수한 경우’ 에 사용할 수 있도록 만들어야 하는 경우 => 합성(조합)을 사용한다.
더 ‘구체적인’ 컴포넌트가 ‘일반적인’ 컴포넌트를 렌더링하고 props를 통해 구체적인 내용을 설정한다.
function Dialog(props) { // 일반적인 컴포넌트
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() { // 구체적인 컴포넌트
return (
<Dialog // 일반적인 컴포넌트를 렌더링 하면서 props를 조정해준다.
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
댓글남기기