State 끌어 올리기(Lifting State Up)



1
2
3
4
5
6
function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}
cs


종종 여러 컴포넌트가 동일한 변경 데이터를 반영해야한다.

이 때는 가장 가까운 상위 컴포넌트가 state값을 관리하는 것이 좋다.


이번 포스팅에서는 주어진 온도에서 물이 끓는지 여부를 계산하는 온도 계산기를 작성한다

코딩은 위의 BoilingVerdict라는 컴포넌트 부터 시작한다.

이 컴포넌트는 섭씨 온도를 받아서 물을 끓일만큼 충분한지 출력한다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }
 
  handleChange(e) {
    this.setState({temperature: e.target.value});
  }
 
  render() {
    const temperature = this.state.temperature;
    return (
      <fieldset>
        <legend>Enter temperature in Celsius:</legend>
        <input
          value={temperature}
          onChange={this.handleChange} />
        <BoilingVerdict
          celsius={parseFloat(temperature)} />
      </fieldset>
    );
  }
}
cs


다음으로 Calculator라는 컴포넌트를 만든다.

온도를 입력할 수 있는 input을 랜더링하고 이 값을 this.state.temperature에 유지한다.

또한 현재 입력값에 대해 BoilingVerdict를 랜더링한다.




두번째 입력 추가


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};
 
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }
 
  handleChange(e) {
    this.setState({temperature: e.target.value});
  }
 
  render() {
    const temperature = this.state.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}
cs


온도 계산기의 새로운 요구사항은 섭씨 뿐만아니라 화씨 입력도 제공하고 동기화 상태를 유지한다는 것이다.

Calculator에서 Temperature 컴포넌트를 추출해서 이를 구현할 수 있다.

"C" 또는 "F" 중에 하나가 될 새로운 Props를 추가하자.




1
2
3
4
5
6
7
8
9
10
class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}
cs


과정이 완료되었으면 Calculator 컴포넌트에 TemperatureInput 2개를 추가한다.

이것은 각각 섭씨와 화씨에 대한 입력 폼이다.

현재는 두 입력값이 동기화 되지 않으므로 요구사항에서 어긋난다.


또한 Calculator는 BoilingVerdict를 표시할 수 없고 현재 온도를 알 수 없다

왜냐하면 이 값들은 TemperatureInput 안에 숨겨져 있기 때문이다.





변환 함수 구현


1
2
3
4
5
6
7
function toCelsius(fahrenheit) {
  return (fahrenheit - 32* 5 / 9;
}
 
function toFahrenheit(celsius) {
  return (celsius * 9 / 5+ 32;
}
cs


동기화를 위해서 첫번째로, 섭씨에서 화씨로 변할수 있는 두가지 함수를 작성한다.

이 두 함수는 숫자를 변환한다.





1
2
3
4
5
6
7
8
9
function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000/ 1000;
  return rounded.toString();
}
cs


다음으로, 문자열 형식인 온도와 방금 작성한 변환 함수를 인자로 받아 문자열을 리턴하는 또 다른 함수를 만든다.

다른 입력을 기반으로 하나의 입력값을 계산하는데 이 값을 사용한다.

이 함수는 잘못된 온도가 들어오면 빈 문자열을 반환하고 출력은 세번째 소수점 이하로 반올림 한다.

예를 들어, tryConvert('abc', toCelsius)는 빈 문자열을 반환하고 

tryConvert('10.22', Fahrenheit)는 '50 .396'을 반환한다.





State 끌어 올리기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }
 
  handleChange(e) {
    this.setState({temperature: e.target.value});
  }
 
  render() {
    const temperature = this.state.temperature;
 
cs


현재 두 TemperatureInput 컴포넌트는 로컬 상태에서 독립적으로 값을 유지한다.

그러나 이 두 값이 동기화 되는것을 구현해야한다.

화씨를 입력하면 섭씨가 동기화 되어야 하며 반대의 경우에도 마찬가지이다.


리액트에서는 state를 컨트롤할 때 state 공유를 필요로하는 컴포넌트들의 가장 가까운 조상에게 위임한다.

이것을 "끌어올리기(Lifting Up)"이라고 한다.

따라서, TemperatureInput에서 로컬 상태를 제거하고 대신 Calculator로 옮긴다.


Calculator가 공유 state를 가지면 두 입력의 현재 온도에 대한 "source of truth"가 된다.

이는 서로에게 일관성 있는 값을 갖도록 지시할 수 있다.

다시 말해, 두 TemperatureInput 컴포넌트의 props가 동일한 상위 Calculator 컴포넌트에서 나오기 때문에 

두 입력이 항상 동기화 된다.





1
2
3
4
  render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
  }    
cs


이것들이 어떻게 단계적으로 동작하는지 살펴보자 

먼저, TemperatureInput의 this.state.temperature를 this.props.temperature로 바꾼다.





1
2
3
  handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
cs


props는 읽기전용 이다.

온도가 로컬 state에 있을 때 TemperatureInput은 this.setState()를 호출하여 변경할 수 있다.

그러나 현재는 값이 상위 컴포넌트에서부터 전달되므로 TemperatureInput에서 변경이 불가능하다.

 

일반적으로 리액트에서는 이와 같은 상황을 컴포넌트를 "제어"함으로써 해결된다.

DOM에서 <input>값이 onChange와 value라는 props를 모두 받아들이듯이

사용자가 만든 TemperatureInput도 temperature와 onTemperatureChange를 모두 받아들일 수 있다.


위의 코드를 추가하면 TemperatureInput이 온도를 변경하려고 할 때

this.props.onTemperatureChange를 호출한다.

온도 변경 핸들러가 호출되면 상위 컴포넌트의 state값이 변경되고 리액트는 이를 감지하여 새로 랜더링을 한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }
 
  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }
 
  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}
cs


Calculator의 변경사항을 살펴보기 전에 TemperatureInput 컴포넌트의 변경에 대해 요약해보자

우선 TemperatureInput안에 있던 로컬 state를 제거하고 상위 컴포넌트로 부터 state값을 받는다.

때문에, 기존에 this.state.temperature값은 this.props.temperature으로 변경된다.


또한 온도값 변경 핸들러도 같이 받아오기 때문에 this.setState()를 사용하는 대신

Calculator에서 받아온 this.props.onTemperatureChange()를 호출한다.





1
2
3
4
5
6
7
8
9
{
  temperature: '37',
  scale: 'c'
}
 
{
  temperature: '212',
  scale: 'f'
}
cs


이제 Calculator 컴포넌트를 살펴보자 

현재 입력의 온도와 스케일을 로컬 state로 저장한다.

이것은 우리가 입력값으로 부터 "들어 올린 상태이며" 두 컴포넌트에 대한 "source of truth"로 작용할 것이다.

이렇게 두 컴포넌트를 랜더링 하기 위해 알아야 할 모든 데이터를 최소한으로 나타낸다.


예를 들어 Celsius에 37을 입력하면 Calculator의 state는 위의 첫번째 코드와 같다.

Fahrenheit에 212를 입력하면 Calculator의 state는 위의 두번째 코드와 같다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }
 
  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }
 
  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }
 
  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
 
    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}
cs


초기에 우리는 두 값을 따로 입력받아 저장하는 것을 고려했지만, 

차례차례 검증을 통해 이것이 불필요한 일임을 알게되었다.

상위 컴포넌트에서 가장 최근에 변경된 입력값과 그것이 나타내는 스케일을 저장하는 것으로 충분하다

이와 같은 방식으로 저장한다면 온도와 스케일 만으로 다른 입력값을 추론할 수 있다.

그리고 입력값은 동일한 상태에서 계산되기 때문에 동기화를 유지한다.


이제 입력값을 편집해도 Calculator의 this.state.temperature 및 this.state.scale이 업데이트 된다.

입력 중 하나가 그대로 값을 가져오므로 모든 사용자 입력이 보존되고 다른 입력값은 이를 기반으로 다시 계산된다.



저작자 표시
신고

입력폼(Forms)



1
2
3
4
5
6
7
<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>
cs


HTML의 폼 엘리먼트는 자연스럽게 내부상태를 유지하기 때문에, 리액트와 조금 다른 방식으로 동작한다.

예를 들어, 일반 HTML 형식의 양식은 단일 이름을 허용한다.

이와 같은 코드를 리액트에서 사용한다면 동일하게 동작은 한다.

그러나 대부분의 경우 form 데이터를 처리하고 

사용자가 form에 입력한 데이터에 액세스 할 수 있는 자바스크립트 기능을 가지는 것이 더 편리하다

이를 위해 controlled component 를 사용한다.





Controlled Component


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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>
    );
  }
}
cs


HTML 에서 <input>,<textarea>,<select>와 같은 form 엘리먼트는 

일반적으로 자체 상태를 유지하고 사용자 입력을 기반으로 업데이트된다.

리액트에서는 변경할수 있는 state가 일반적으로 컴포넌트의 state의 속성에 유지 되며 setState()만 업데이트된다.


우리는 리액트의 state를 "single source of truth"로 만들어 두 엘리먼트를 결합할 수 있다.

그런다음 form을 랜더링 하는 리액트 컴포넌트는 추후 사용자 입력시 해당 form 에서 일어나는 일을 제어한다.

이러한 방식으로 리액트에 의해 제어되는 form 엘리먼트를 Controlled Component라고 한다.


예를 들어 위의 코드에서 처럼 이전 예제가 제출 될 때 이름을 기억되도록 하려면,

controlled component로 form을 작성할 수 있다.


value 속성은 form 엘리먼트에 설정되어 있으므로 표시된 값은 

항상 this.state.value가 되어 리액트 state가 single truth가 된다.

리액트 state를 업데이트 하기 위해 모든 키 입력에서 handleChange가 실행되기 때문에

사용자가 입력할 때 마다 표시된 값이 업데이트 된다.





<textarea> 태그


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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>
          Name:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
 
cs


HTML에는 <textarea></textarea> 사이에 있는 글자가 페이지에 출력된다.

JSX에서는 <textarea />의 value로 페이지에 출력되는 글자를 전달한다.





<select> 태그


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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>
    );
  }
}
cs


HTML에서 <select>는 <option>과 결합하여 드롭다운 메뉴를 만든다.

<select>에서 제일 최상단의 option이 초기 select 메뉴로 지정되는데, 

리액트는 이와 다르게 select에 value로 전달된 값이 초기 select 메뉴가 된다.

따라서 한곳만 업데이트하면 되기 때문에 controlled component에서 더 편리하다


전반적으로 이렇게하면 <textarea>와 <select>는 매우 유사하게 작동한다.

두 태그는 모두 controlled component를 구현하는데 사용될 수 있는 값 속성을 허용한다.





다중 입력 처리


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };
 
    this.handleInputChange = this.handleInputChange.bind(this);
  }
 
  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
 
    this.setState({
      [name]: value
    });
  }
 
  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}
cs


여러개의 입력 엘리먼트를 처리해야 하는 경우 각 엘리먼트에 name 속성을 추가하고 

event.target.name의 값에 따라 수행할 작업을 선택하게 할 수 있다.



저작자 표시
신고

리스트와 키(Lists and Keys)



1
2
3
const numbers = [12345];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
cs

먼저, 자바스크립트를 리스트로 변환하는 것을 보자

위의 코드를 사용하면 map() 함수를 사용하여 숫자 배열을 가져와서 두배로 늘릴수 있다.

map()에 의해 반환 된 새로운 배열을 double 변수에 할당하고 그것을 기록한다.

따라서 콘솔에는 [2,4,6,8,10]이 출력된다.

리액트에서 엘리먼트를 리스트에 변환하는 것은 이것과 가의 동일하다





여러 컴포넌트 랜더링


1
2
3
4
5
const numbers = [12345];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);
 
cs


엘리먼트 콜렉션을 빌드하고 중괄호 {}를 사용하여 JSX에 포함이 가능하다.

위의 예제 코드에서는 자바스크립트 map() 함수를 사용하여 숫자 배열을 반복한다.

각 항목에 대해 <li> 요소를 반환하고, 엘리멘트 배열을 listitem에 할당한다




1
2
3
4
ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);
cs


이렇게 반환된 listitem 배열을 <ul> 태그 안에 넣고 DOM에 랜더링한다.

결과적으로 위의 코드는, 실행되면 1과 5 사이의 숫자로 된 글 머리표 목록을 표시한다.





기본적인 리스트 컴포넌트


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li>{number}</li>
  );
  return (
    <ul>{listItems}</ul>
  );
}
 
const numbers = [12345];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);
cs


우리는 앞의 예제에서 숫자를 허용하고 정렬되지 않은 엘리먼트 목록을 출력하는 컴포넌트로 리팩토링 할 수 있다.

이 코드를 실행하면 엘리먼트 목록에 키가 제공되어야한다는 경고가 표시된다.

키는 엘리먼트 목록을 만들 때 포함해야하는 특수 문자열 속성이다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}
 
const numbers = [12345];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);
cs


numbers.map() 내부의 키를 지정하여 에러가 난 문제 코드를 수정하자.





키(Key)


1
2
3
4
5
6
const numbers = [12345];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);
cs


키는 리액트가 아이템의 변경사항, 추가, 제거를 식별할 수 있도록 도움을 준다.

엘리먼트에 안정적인 ID를 제공하려면 배열 내부 엘리먼트에 키를 지정해야합니다.




1
2
3
4
5
const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);
cs


키를 선택하는 가장 좋은 방법은 형제 중 리스트를 고유하게 식별하는 문자열을 사용하는 것이다.

대부분의 경우 데이터의 ID를 키로 사용한다.




1
2
3
4
5
6
const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);
cs


랜더링 된 항목에 대한 안정적인 ID를 설정할 수 없는 경우, 

최후의 수단으로 색인을 키로 사용할 수 있다.





키로 컴포넌트 추출


키는 배열의 컨텍스트 내부에서만 의미를 가진다

예를 들어, 아래의 코드에서 ListItem 컴포넌트를 추출하는 경우,

ListItem 자체의 루트 <li> 요소가 아니라 배열의 <ListItem /> 엘리먼트에 키를 보관해야한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function ListItem(props) {
  const value = props.value;
  return (
    // Wrong! There is no need to specify the key here:
    <li key={value.toString()}>
      {value}
    </li>
  );
}
 
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Wrong! The key should have been specified here:
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
 
const numbers = [12345];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);
cs


위의 예제는 잘못된 key 사용 방법이다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function ListItem(props) {
  // Correct! There is no need to specify the key here:
  return <li>{props.value}</li>;
}
 
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // Correct! Key should be specified inside the array.
    <ListItem key={number.toString()}
              value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
 
const numbers = [12345];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);
cs


위의 예제는 올바른 key 사용 방법이다.

즉, map 함수 내부에서 key를 포함해 전달해 줘야한다.




키는 동일한 컴포넌트들 사이에서 유일성을 가져야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}
 
const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('root')
);
cs


배열내에서 사용하는 키는 유일해야한다.

그러나 전역적으로 유일성을 가질 필요는 없다.

두개의 다른 배열을 생성할 때 동일한 키를 사용할 수 있다.




1
2
3
4
5
6
const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);
cs


키는 리액트에서 힌트 역할을 하지만 컴포넌트 엘리먼트로 전달되지는 않는다.

컴포넌트에 동일한 값이 필요하면 명시적으로 다른 이름의 props로 전달해야한다.

예를 들어, 위의 예에서 id 값을 읽을 수 있지만, key값은 읽을 수 없다.





JSX에서 map() 사용하기


1
2
3
4
5
6
7
8
9
10
11
12
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <ListItem key={number.toString()}
              value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
cs


위의 예제에서 우리는 별도의 listitems 변수를 선언하고 이를 JSX에 포함시켰다.




1
2
3
4
5
6
7
8
9
10
11
function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}
cs


JSX는 map()의 결과를 인라인으로 표현 할 수 있도록 모든 expression을 중괄호 내에 표현할 수 있다.

상황에 따라 이 코드가 명확해 질 수 있지만 스타일이 악용될 수 있다.

자바스크립트와 마찬가지로 가독성을 위해 변수를 추출할 가치가 있는지는 개발자가 결정해야한다.

map() 함수가 너무 중첩되어있으면 마찬가지로, 컴포넌트를 분리하는 것이 좋다.


저작자 표시
신고

조건부 랜더링(Conditional Rendering)



1
2
3
4
5
6
7
function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}
 
function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}
cs


리액트에서는 필요한 동작을 캡슐화하는 컴포넌트를 만들 수 있다.

그런 다음 애플리케이션의 state에 따라 일부만 랜더링 할 수 있다.


리액트의 조건부 랜더링은 자바스크립트에서와 동일하게 동작한다.

if와 같은 자바스크립트의 조건부 연산자를 사용하여 현재 state를 나타내게하는 엘리먼트를 만들고

리액트가 UI를 업데이트 하여 일치하도록 한다.


사용자의 로그인 여부를 출력하는 위의 두 컴포넌트를 만들었다고 했을 때,

이를 관리하는 상위 컴포넌트를 만들어야한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
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')
);
cs


Greeting이 상위 컴포넌트로써 동작하게되는데,

사용자가 로그인 했는지의 여부를 조건문을 통해 판단하므로 결과값에 따라 출력되는 값도 달라진다.

결과적으로, 이 예제는 proprs. isLoggedIn 값에 따라 다른 결과물을 출력한다.





엘리먼트 변수


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function LoginButton(props) {
  return (
    <button onClick={props.onClick}>
      Login
    </button>
  );
}
 
function LogoutButton(props) {
  return (
    <button onClick={props.onClick}>
      Logout
    </button>
  );
}
cs


리액트에서는 변수를 사용해서 엘리먼트를 저장할수 있다.

이렇게 하면 출력의 나머지 부분이 변경되지 않는 동안 조건부로 컴포넌트의 일부를 랜더링하는데 도움이 될 수 있다.

위의 코드는 로그인과 로그아웃 버튼을 나타낸다





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.handleLoginClick = this.handleLoginClick.bind(this);
    this.handleLogoutClick = this.handleLogoutClick.bind(this);
    this.state = {isLoggedIn: false};
  }
 
  handleLoginClick() {
    this.setState({isLoggedIn: true});
  }
 
  handleLogoutClick() {
    this.setState({isLoggedIn: false});
  }
 
  render() {
    const isLoggedIn = this.state.isLoggedIn;
 
    let button = null;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }
 
    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}
 
ReactDOM.render(
  <LoginControl />,
  document.getElementById('root')
);
cs


위의 코드는 이렇게 만들어진 두개의 로그인/로그아웃 버튼을 조건에 따라 컨트롤 하는 예제이다

LoginControl의 현재 state 값에 따라서 로그인/로그아웃 버튼을 출력하게 된다.

또한 제일 위의 예제인 Greeting 도 관리하게 된다.


변수를 선언하고 if로 조건부 랜더링을 하는 것은 좋은 방법이지만 더 짧은 구문을 사용하는 것이 좋다.

아래의 그 방법들에 대한 예제들이다.  




&& 논리 연산자 사용


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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')
);
cs


JSX에서는 중괄호로 묶어서 expression을 삽입할 수 있다. 여기에는 Javascript && 논리 연산자가 포함된다.

컴포넌트를 조건부로 포함하는 것이 편리 할 수 있다.


자바스크립트에서 true&&expression은 항상 expression으로 결과값이 나오며,

false&&expression은 항상 false로 결과값이 나오기 때문에 위의 코드는 무리 없이 잘 동작한다.


따라서 조건이 참 이면 && 다음에 오는 엘리먼트가 출력에 나타나고 거짓이면 리액트는 해당 표현식을 건너 뛴다.




if-else 3항 조건 연산자 사용


1
2
3
4
5
6
7
8
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}
cs


엘리먼트를 조건부로 랜더링하는 방법은 자바스크립트 3항 조건 연산자를 사용하는 것이다.

위의 예제는 isLoggedIn의 값에 따라 문자열값을 다르게 삽입하는 것을 보여준다.




1
2
3
4
5
6
7
8
9
10
11
12
render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
      ) : (
        <LoginButton onClick={this.handleLoginClick} />
      )}
    </div>
  );
}
cs


단순히 문자열이 아닌 더 큰 표현식에도 사용될 수 있지만, 상황이 덜 명확하다.

자바스크립트에서와 마찬가지로 프로젝트를 운영하는 팀이 보다 가독성 있는 코드를 추구한다면 

적절한 스타일을 선택하는 것은 팀의 컨벤션에 달려있다


또한 조건이 더욱 복잡해 질 때마다 컴포넌트를 적절히 추출하여 분리하는것이 좋다.





랜더링에서 컴포넌트 방지


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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')
);
cs


드문 경우이지만, 컴포넌트가 다른 컴포넌트에 의해 랜더링 되었더라도 컴포넌트를 숨길 수  있다.

이렇게 하려면 랜더링 출력 대신 null을 반환한다.

위의 예제에서는 warn이라는 props 값에 따라 <WanningBanner />를 랜더링한다.

이 prop 값이 false인 경우 컴포넌트는 랜더링을 실행하지 않는다.


컴포넌트의 랜더링 메소드에서 null을 반환해도 컴포넌트의 라이프 사이클 메소드를 호출하는데 영향을 주지 않는다.

예를 들어, componentDidMount와 componentWillUnmount는 여전히 호출된다.





저작자 표시
신고

이벤트 핸들링(Event Handling)




1
2
3
4
5
6
7
<button onclick="activateLasers()">
  Activate Lasers
</button>
 
<button onClick={activateLasers}>
  Activate Lasers
</button>
cs


리액트 엘리먼트를 사용해서 이벤트를 처리하는 것은 DOM 엘리먼트에서 이벤트를 처리하는 것과 매우 유사하다

1) 반응 이벤트는 소문자가 아니라 Camel Case를 사용한다.

2) JSX에서는 문자열이 아닌 이벤트 처리기로 함수를 전달한다.


예를 들어 첫번째는 기존에 HTML 에서 이벤트를 처리하는 방식이었다.

두번째는 JSX에서 이벤트를 처리하는 방식인데 중괄호 안에 Camel Case로 이벤트를 명시하게된다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>
 
 
function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }
 
  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}
cs


또다른 차이점은, 리액트에서 기본 동작을 방지하기 위해서 false를 반환할 수 없다는 것이다.

그리고 preventDefault를 명시적으로 호출해야한다.

예를 들어, HTML 일반 새 페이지를 여는 기본 링크 동작을 방지하기 위해서 첫번째 코드처럼 코딩할 수 있다.

그러나 리액트에서는 두번째 방식처럼 코딩 해야한다.


여기서 e는 synthetic 이벤트이다

리액트는 W3C 스펙에 따라 이러한 synthetic 이벤트를 정의하므로 브라우저 간 호환성에 대해 걱정할 필요가 없다

자세한 내용은 synthetic 이벤트 가이드 페이지를 참조하기 바란다.

https://facebook.github.io/react/docs/events.html





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};
 
    // This binding is necessary to make `this` work in the callback
    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')
);
cs


ES6 클래스를 사용하여 컴포넌트를 정의 할 때 공통 패턴은 이벤트 핸들러가 클래스의 메소드가 되게 하는 것이다.

예를 들어, 위 코드의 Toggle 컴포넌트는 사용자가 "ON"과 "OFF"상태 사이를 전환할 수 있게 하는 버튼을 랜더링한다.


Toggle 클래스의 render 메소드 내부 JSX에서 this에 주목 해야한다.

자바스크립트에서는 클래스 메소드가 기본적으로 바인딩 되지 않는다.

따라서 this.handleClick을 바인딩 하고 onClick에 전달하는 것을 잊어버리면 함수가 호출될 때 에러가 발생한다.

이것은 리액트의 특징이 아니라 자바스크립트의 특징이다.

일반적으로 onClick = {this.handleClick}처럼 그 뒤에 ()가 없는 메소드를 참조하는 경우 해당 메소드를 바인딩 해야한다.


저작자 표시
신고

State와 라이프 사이클



지금까지 우리는 랜더링을 업데이트하는 한가지 방법만을 배웠다.

바로 ReactDom.render()를 호출하여 랜더링된 출력을 변경하는 것이다.

이번 포스팅에서는 앞서 만든 시계 예제를 재사용가능하고 캡슐화 시키는 방법에 대해서 알아볼 것이다.

변경 후에는 저절로 시간이 변경될때마다 자동으로 랜더링 될 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}
 
function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}
 
setInterval(tick, 1000);
cs


먼저 위의 코드와 같이 Clock 컴포넌트를 만드는 것으로 캡슐화를 시작할 수 있다.

또한, tick함수가 아닌 Clock에서 시간을 업데이트해줘야한다.

이를 구현하려면 시간에 대한 state를 Clock 컴포넌트에 추가해줘야한다.

state는 props와 비슷하지만 컴포넌트에서 컨트롤할 수 있다는 것이 차이점이다.





함수를 클래스로 변환


1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
cs


Clock과 같은 functional component를 5단계에 걸쳐서 클래스로 변환할 수 있다.


1. React.Component를 상속받는(extends) 동일한 이름의 ES6 클래스를 만든다

2. render라는 빈 메소드를 추가한다.

3. 함수 본문을 render 메소드로 옮긴다.

4. props를 render 메소드 본문의 this.props로 변경한다.

5. 나머지 빈 함수 선언을 삭제한다.


이 과정을 마치면 Clock은 이제 함수가 아닌 클래스로 선언된다.




클래스에 로컬 state 추가


클래스가 완성되었으니 우리는 이제 3단계로 props를 state로 옮길것이다.


1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
cs


1) render() 메소드에서 this.props.date를 this.state.date로 바꾼다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>
    );
  }
}
cs


2) this.state를 초기화 하는 constructor를 추가한다.

여기서 constructor에 props가 인자로 들어간것을 주목하자.

그리고 반드시 constructor안에서 super()를 호출해야한다.





1
2
3
4
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
cs


3) Clock 컴포넌트를 호출하는 태그에서 date를 제거한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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')
);
cs


결과적으로 다음과 같은 코드로 변경 된다.




클래스에 라이프 사이클 메소드 더하기


많은 컴포넌트들로 구성된 어플리케이션에서는 

컴포넌트가 사라질 때 사용된 리소스를 확보하는 것은 매우 중요하다. 


어떤 컴포넌트가 DOM에 처음 랜더링 될 때 이것을 리액트에서는 "마운트"라고 한다.

또한 컴포넌트가 DOM이 제거될 때 "마운트 해제" 또는 "언마운트"라고 한다.


컴포넌트가 마운트 및 마운트 해제될 때 특정 코드를 실행하기 위해서 

컴포넌트 클래스에 특수 메소드를 선언할 수 있다.


이러한 특수 메소드를 "라이프 사이클 후크(lifecycle hooks)"라고 한다.




1
2
3
4
5
6
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }
cs


componentDidMount() 메소드는 컴포넌트가 DOM에 랜더링 된 후에 실행이 된다.

Clock에서 타이머를 설정하기에 좋은 장소이다.





1
2
3
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
cs


그리고 componentWillUnmount() 라이프 사이클 후크에서 타이머를 종료한다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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>
    );2)
  }
}
 
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);
 
cs


마지막으로 매초 마다 실행되는 함수를 tick에 선언한다.

여기서 state를 갱신하기 위해 this.setState 메소드를 사용한다.

절대 직접 접근으로 state를 변경해서는 안된다. 이는 불변성(immutable)에 대한 위반이다.

변경이 완료되면 매 초 마다 시계가 돌아간다.


무슨일이 일어나고 있는지 간단하게 요약하면 다음과 같다.


1) <Clock />이 ReactDOM.render에 전달되면 React가 Clock 컴포넌트의 constructor를 호출한다.

constructor에서는 this.state를 현재 시간을 포함하는 객체로 초기화한다.


2) 그 후 Clock 컴포넌트 내의 render 메소드를 호출한다. 

그런 다음 리액트는 Clock의 render 출력과 동일하도록 DOM을 업데이트한다.


3) Clock 출력이 DOM에 삽입되면 리액트는 componentDidMount() 메소드를 호출한다.

그 안에 Clock 컴포넌트가 브라우저에 tick()을 한번씩 호출하는 타이머를 설정하도록 요청한다.


4) 매 초마다 브라우저는 tick() 메소드를 호출한다.

그 안에 Clock 컴포넌트는 현재 시간을 포함하는 객체로 setState()를 호출하여 UI 업데이트를 예약한다.

setState() 메소드 덕분에 리액트는 상태가 변경된것을 감지할 수 있고 render() 메소드를 호출하여 DOM을 업데이한다.


5) Clock 컴포넌트가 DOM에서 제거 되면 React가 componentWillUnmount() 메소드를 호출하여 타이머가 중지된다.





State 올바르게 사용하기


setState에 대해 알아야할 3가지가 있다.


1) state를 직접 변경하지 않을 것

state를 직접 변경하면 리액트가 이를 감지하고 다시 랜더링 하지 않으므로 setState를 이용한다.

state를 유일하게 할당 할수 있는 곳은 constructor다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
 
// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));
 
// Correct
this.setState(function(prevState, props) {
  return {
    counter: prevState.counter + props.increment
  };
});
cs


2) state 업데이트는 비동기일 수도 있다.

리액트는 여러 setState()의 호출 성능 향상을 위해 단일 업데이트로 배치할 수 있다.

따라서 this.props 및 this.state가 비동기적으로 갱신될 수 있으므로 state를 계산할때 값을 신뢰하면 안된다.


따라서 위의 첫번째 코드는 업데이트 되어지지 못할수도 있다.

이 문제를 해결하기 위해서는 객체가 아닌 함수를 받아들이는 두번째 방법을 사용하는것이 좋다.

이 함수는 이전상태를 첫번째 인자로 받고 업데이트된 props를 두번째 인자로 받는다.

두번째 방법은 arrow function을 사용했지만, 세번째처럼 일반 함수에서도 동일하게 동작한다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }
 
  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });
 
    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }
cs


3) state 업데이트가 병합된다.


setState() 메소드를 호출하면 리액트가 제공한 객체를 현재 상태로 병합한다.

예를들어 사용자의 state에는 독립된 변수가 포함될수 있다.

이 때, state 내부의 독립된 변수를 각각 업데이트 한 후 state에 병합 할 수 있다는 것이다.



저작자 표시
신고

'WEB > ReactJS' 카테고리의 다른 글

[ReactJS] 조건부 랜더링(Conditional Rendering)  (0) 2017.06.13
[ReactJS] 이벤트 핸들링(Event Handling)  (0) 2017.06.13
[ReactJS] State와 라이프 사이클  (0) 2017.06.13
[ReactJS] 컴포넌트와 Props  (0) 2017.06.12
[ReactJS] 랜더링(Rendering)  (0) 2017.06.12
[ReactJS] JSX  (0) 2017.06.12

컴포넌트와 Props



리액트의 가장 큰 장점은 컴포넌트의 조합으로 UI를 짜는 것이다.

리액트에서는 컴포넌트를 사용하여 UI를 독립적이고 재사용 가능한 부분으로 분리하며,

각 부분을 개별적으로 생각할 수 있다.


개념상 컴포넌트는 javascript의 function과 같다.

이 때, 컴포넌트 내부의 props는 임의의 입력을 받아들이고, 

무엇이 화면에 나타나야하는지를 설명하는 리액트 엘리먼트를 반환한다.





함수와 클래스 컴포넌트


1
2
3
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
cs


컴포넌트를 가장 쉽게 정의 하는 방법은 function을 사용하는 것이다.

이 함수는 하나의 props 객체를 인수로 받아들이고 React 엘리먼트를 반화하기 때문에 유효한 컴포넌트이다.

이러한 컴포넌트는 말그대로 자바스크립트의 함수와 동일하기 때문에 functional component라고 한다.




1
2
3
4
5
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
cs


사용자는 functional component 뿐만 아니라 ES6에서 제공하는 class로 컴포넌트를 만들 수 있다.

위 두개의 컴포넌트는 리액트의 관점에서 모두 동일한 컴포넌트이다.


클래스의 추가 기능에 대해서는 다음 포스팅에서 다루기로 하고 

우선 props 의 개념과 기능을 알아보기 위해 이번 포스팅에서는 functional component를 사용한다.





컴포넌트 랜더링


이전에는 엘리먼트에 DOM 태그만 나타냈었다.

그러나 엘리먼트는 사용자 정의 컴포넌트를 나타낼 수 도 있다.

리액트가 사용자 정의 컴포넌트를 나타내는 엘리먼트를 볼 때 JSX 속성을 이 컴포넌트에 단일 객체로 전달한다.

우리는 이 객체를 Props라고 부른다.


1
2
3
4
5
6
7
8
9
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
 
const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);
cs


예를 들어서 위의 코드는 페이지에 Hello, Sara를 랜더링한다.

이 예제에서 일어나는 일을 요약해 보면


1. ReactDom.render()를 <Welcome name="Sara">와 함께 호출한다.

2. 리액트는 {name:"Sara"}가 있는 Welcome 구성 요소를 props로 호출한다.

3. Welcome 컴포넌트는 결과로 <h1>Hello, Sara</h1> 엘리먼트를 반환한다.

4. React DOM은 <h1>Hello, Sara</h1>과 일치하도록 DOM 을 업데이트한다.


주의할점은 항상 대문자로 컴포넌트를 작성해야하는것이다.

소문자로 작성된 태그들은 DOM 태그를 나타내며, 대문자는 사용자 정의로 만들어진 컴포넌트를 나타낸다.





컴포넌트 작성


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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')
);
cs


컴포넌트는 출력에서 다른 컴포넌트를 참조할 수 있다.

이를 통해 모든 세부 단계에서 동일한 컴포넌트의 추상화 사용이 가능하다.

button, form, dialog, screen 등, 리액트에서는 모든것이 컴포넌트로 동작한다.

예를 들어서 위의 코드처럼, Welcome을 여러번 랜더링하는 코드를 작성하는것이 가능하다.


일반적으로 모든 리액트 앱은 맨위에 단일 App 구성요소가 존재한다.

그러나 리액트를 기존앱에 통합하는 경우 Button과 같은 작은 컴포넌트를 사용하여 상향식으로 시작하고

점차적으로 계층의 제일 상위로 올라갈 수 있다. 





컴포넌트 추출


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}
cs


리액트는 작은 컴포넌트를 쌓아서 만들기 때문에 컴포넌트를 분할하는것을 두려워해서는 안된다.

예를 들어 위의 코드에서 Comment 컴포넌트를 보자.

여기서는 author,text, date을 props로 받아들여서 SNS에 대한 설명을 출력한다.


이 컴포넌트는 중첩으로 인해 변경하기가 까다로울 수 있으며 개별 부분을 재 사용하기도 어렵다.

여기서 몇가지 컴포넌트들을 추출해 보자.





1
2
3
4
5
6
7
8
function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}
cs


먼저 Avatar를 추출한다.

Avatar는 Comment 내부에서 랜더링 되고 있음을 알 필요가 없다.

때문에 컴포넌트에 Avatar라는 일반적인 이름을 부여했다.

컴포넌트의 관점에서 사용되는 컨텍스트가 아닌 컴포넌트의 이름을 지정하는것이 좋다.





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}
cs


우리는 이제 분리한 Avatar를 이용해서 좀 더 심플한 Comment를 만들수 있다.





1
2
3
4
5
6
7
8
9
10
function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}
cs


다음으로 사용자 이름 옆에 Avatar를 랜더링하는 UserInfo 컴포넌트를 추출한다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}
 
cs


이를 통해서 우리는 comment를 더욱 단순화 할 수 있다.

컴포넌트를 추출하는 것은 처음보기에 상당히 불필요한 일처럼 보일수가 있다.

그러나 이렇게 세분화하지 않으면 재사용성이 떨어지고, 유지보수 시에도 큰 비용을 지불해야한다.


추출하기 위한 기준으로, UI가 반복되거나 컴포넌트가 충분히 복잡하면

분리되어야 할 필요성이 있다.


저작자 표시
신고

'WEB > ReactJS' 카테고리의 다른 글

[ReactJS] 이벤트 핸들링(Event Handling)  (0) 2017.06.13
[ReactJS] State와 라이프 사이클  (0) 2017.06.13
[ReactJS] 컴포넌트와 Props  (0) 2017.06.12
[ReactJS] 랜더링(Rendering)  (0) 2017.06.12
[ReactJS] JSX  (0) 2017.06.12
[ReactJS] Hello world!  (0) 2017.06.12

랜더링(Rendering)



엘리먼트는 리액트 어플리케이션에서 가장 작은 단위이다.

엘리먼트는 화면에서 보여지는 것에 대한 정보를 가지고 있다.

브라우저 엘리먼트인 DOM 엘리먼트와는 달리, 리액트 앨리먼트는 일반 객체이므로 값 싸게 만들수 있다.

React DOM은 React 앨리먼트와 일치하도록 DOM을 업데이트 한다.


리액트에서 앨리먼트와 컴포넌트를 헷갈릴수 있는데, 

컴포넌트에 대한 자세한 설명은 다음 포스팅에서 다루도록한다.

그전에, 간단하게 설명하자면 엘리먼트는 컴포넌트가 무엇인지를 나타낸다고 할 수 있다.




엘리먼트를 DOM에 랜더링 하기


1
2
3
4
5
const element = <h1>Hello, world</h1>;
ReactDOM.render(
  element,
  document.getElementById('root')
);
cs


우리가 만드는 웹페이지에 div라는 HTML 태그가 있다고 가정하자.

우리는 이것은 root 노드라고 부른다.

이 태그 내부의 모든것이 React DOM에 의해서 관리되기 때문이다.


React로 구축된 애플리케이션은 대게 단일 루트의 DOM 노드를 사용한다.

React를 기존앱에 통합하게 되는 경우 원하는 만큼 만은 분리된 루트 DOM 노드가 생성 될 수 있다.

위의 코드를 실행하면 Hello, world가 출력된다.





랜더링 된 엘리먼트 업데이트


1
2
3
4
5
6
7
8
9
10
11
12
13
14
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(
    element,
    document.getElementById('root')
  );
}
 
setInterval(tick, 1000);
cs


리액트의 엘리먼트는 불변(immutable)하다.

따라서 엘리먼트를 만든 후에 자식 또는 특성을 변경할 수 없다.

엘리먼트는 영화의 단일프레임과 같아서 특정 시점의 UI를 나타낸다.


지금까지 배운 내용으로 UI를 업데이트하는 유일한 방법은, 

새 엘리먼트를 만들어서 ReactDOM.render()에 전달하는 것이다.


이것에 대한 예제가 바로 위의 시간을 출력하는 코드이다.

1초마다 Date값을 받아와서 페이지에 시간을 랜더링하게 된다.


그러나 실제로, 대부분의 React 앱은 ReactDom.render()를 한번만 호출한다.

다음 포스팅에서는 이러한 코드가 어떻게 캡슐화 되는지에 대해서 알아볼 것이다.



저작자 표시
신고

'WEB > ReactJS' 카테고리의 다른 글

[ReactJS] State와 라이프 사이클  (0) 2017.06.13
[ReactJS] 컴포넌트와 Props  (0) 2017.06.12
[ReactJS] 랜더링(Rendering)  (0) 2017.06.12
[ReactJS] JSX  (0) 2017.06.12
[ReactJS] Hello world!  (0) 2017.06.12
[ReactJS] 튜토리얼(Tutorial) - 10  (0) 2017.06.11

JSX



1
const element = <h1>Hello, world!</h1>;
cs

다음과 같은 변수 선언을 고려해보자.

이 <h1> 태그는 html이나 문자열이 아니다.

이것은 JSX라고 불리는 자바스크립트의 새로운 확장된 문법이다.

리액트 개발자들은 리액트 라이브러를 통해 UI를 표현 하기 위해서 JSX 문법을 사용하기를 추천한다.

JSX를 템플릿 문법이라고 생각할 수도 있지만, 어느 문법보다 강력한 기능을 제공한다.


가장 중요한 점은 JSX는 리액트의 앨리먼트를 생성한다는 것이다.

다음 포스팅에서 엘리먼트를 HTML DOM에 랜더링하는 방법이 있을 것이고,

우선은 JSX를 사용하는 방법에 대해서 알아보도록하자.

 




JSX에 자바스크립트 문법 포함하기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}
 
const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};
 
const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);
 
ReactDOM.render(
  element,
  document.getElementById('root')
);
cs


사용자는 JSX를 사용할 때 {}안에 자바스크립트 문법을 넣을수 있다.

위의 코드에서 {formatName(user)} 등이 JSX에 포함된 자바스크립트의 문법이다.


이 코드에서 주목할만한 점은 가독성을 위해서 JSX를 여러 라인에 걸쳐 분리해 놓은 것이다.

또한 리액트 개발자들은 필수 사항은 아니지만 JSX를 괄호에 싸서 처리하는 것을 추천한다.





JSX도 표현식이다.


1
2
3
4
5
6
function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}
cs


컴파일이 끝나면 JSX는 일반적인 자바스크립트 객체가 된다.

즉, if문과 for문에서 JSX를 사용할 수 있고 변수에 할당하고 인수에 받아들여 리턴까지 가능하다.

다시 말해 1급객체의 속성을 가진다고 할 수 있다.





JSX로 속성(Attribute) 지정하기


1
2
const element = <div tabIndex="0"></div>;
const element = <img src={user.avatarUrl}></img>;
cs


사용자는 JSX에서 속성을 지정할 수 있다.

예를 들어 1번줄 처럼 따옴표를 사용하여 문자열을 리터럴 속성으로 지정하거나

중괄호를 사용하여 속성에 자바스크립트 문법을 포함 시킬수도 있다.

이때 중괄호를 따옴표로 묶으면 문자열 처리되므로 주의하기 바란다.

속성을 지정하기 위해서 따옴표나 중괄호로 묶어야하지만 둘다 같은 속성에 사용해야하는 것은 아니다


또한 JSX는 HTML 보다 자바스크립트에 가깝기 때문에 

속성 이름을 지정할 때 HTML 속성이름이 아닌 camelCase(카멜 표기법)로 지정해야한다.

예를 들어, class는 className, tabindex는 tabIndex가 된다.





JSX는 Insection 공격을 예방한다.


1
2
3
4
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
 
cs


위의 코드에서는 사용자 입력을 JSX로 입력받는 것이 안전하다.

기본적으로, React DOM은 랜더링 하기 전에 JSX에 포함된 모든 값을 escape처리한다.

따라서 응용프로그램에 명시적으로 작성되지 않은 것은 삽입할 수 없다.

다시 말해, 모든 것은 랜더링 되기전에 문자열로 변경되는데 이를 통해 XSS(사이트간 스크립팅) 공격을 예방할 수 있다.





JSX는 객체를 나타낸다


1
2
3
4
5
6
7
8
9
10
11
12
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
 
 
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
cs


Babel은 JSX를  React.createElement 로 컴파일한다.

따라서 위 코드의 두 element 변수는 동일한 의미이다.




1
2
3
4
5
6
7
8
// Note: this structure is simplified
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world'
  }
};
cs


React.createElement는 버그없이 깔끔한 코드를 작성하는데 도움이 되는 몇가지 검사를 수행하지만,

기본적으로 위와 같은 객체를 만든다.

이 객체를 react elements라고 하며 화면에서 보고 싶은 내용에 대한 설명으로 생각할 수 있다.

리액트는 이러한 객체를 읽고 이를 사용하여 DOM을 구성하고 최신 상태를 유지한다.








저작자 표시
신고

'WEB > ReactJS' 카테고리의 다른 글

[ReactJS] 컴포넌트와 Props  (0) 2017.06.12
[ReactJS] 랜더링(Rendering)  (0) 2017.06.12
[ReactJS] JSX  (0) 2017.06.12
[ReactJS] Hello world!  (0) 2017.06.12
[ReactJS] 튜토리얼(Tutorial) - 10  (0) 2017.06.11
[ReactJS] 튜토리얼(Tutorial) - 9  (0) 2017.06.11

Hello world!



튜토리얼부터 포스팅하고 기능을 설명하는건 순서가 안맞는 것 같지만,

리액트에 대한 기본 지식이 없다면 여기서 라이브러리 사용방법을 익히고 튜토리얼로 돌아가도 좋다.


어떤 언어를 익히건 초기에는 콘솔창에 Hello world를 띄우기 마련이다

똑같은 원리로, 리액트에서도 리액트만의 문법을 사용하여 Hello world를 출력해 보도록하자.


그전에, 리액트는 로컬에서 실행환경을 갖추고 작업해도 되지만,

Codepen이라는 브라우저 기반 환경에서도 원활히 돌아가기 때문에, 실습 시 이 사이트를 추천한다.

Hello world를 미리 짜놓은 페이지는 다음과 같다.

https://codepen.io/gaearon/pen/ZpvBNJ?editors=0010



1
2
3
4
ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);
cs


리액트에서 Hello world를 출력하는 코드는 다음과 같이 나타내어 질 수 있다.

ReactDom에서 JSX라고 불리는 문법을 이용하여, HTML 코드 어딘가에 자신이 만든 컴포넌트를 삽입하는 것이다.

JSX와 컴포넌트, 앨리먼트 더 나아가서 prop와 state에 대해 점차적으로 살펴 볼것이며,

추후에는 이를 이용해 작은 컴포넌트들로 부터 큰 프로젝트를 진행할 수 있을 것이다.


한가지 중요한 점은 리액트는 자바스크립트 라이브러리이므로 자바스크립트에 대한 문법을 알아야 한다는 것이다.

만약 자바스크립트에 대한 공부가 부족하다고 생각하면 MDN이나 생활 코딩등에서 기초문법을 공부하고 오기 바란다.


또한 좀더 심화적인 문법으로 2015년 출시된 자바스크립트의 공통 표준인 ES6 문법을 지원하기 때문에

ES6의 일부 대표적 문법에 대해서도 숙지해야한다.



저작자 표시
신고

'WEB > ReactJS' 카테고리의 다른 글

[ReactJS] 랜더링(Rendering)  (0) 2017.06.12
[ReactJS] JSX  (0) 2017.06.12
[ReactJS] Hello world!  (0) 2017.06.12
[ReactJS] 튜토리얼(Tutorial) - 10  (0) 2017.06.11
[ReactJS] 튜토리얼(Tutorial) - 9  (0) 2017.06.11
[ReactJS] 튜토리얼(Tutorial) - 8  (0) 2017.06.10

+ Recent posts

티스토리 툴바