본문 바로가기

4. Node.js | React.js

4/22(토) IT K-DT(-) / 6. Component와 Props~10.리스트와key

 

6. Component와 Props

 

6-1. 함수 컴포넌트와 클래스 컴포넌트

컴포넌트를 정의하는 가장 간단한 방법은 JavaScript 함수를 작성하는 것.


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

이 함수는 “props” (속성을 나타내는 데이터) 객체인자를 받은 후 React 엘리먼트를 반환함.

→ 유효한 React 컴포넌트임.

이러한 컴포넌트는 'JavaScript 함수'이기 때문에 말 그대로 “함수 컴포넌트”라고 호칭.

ES6 class를 사용하여 컴포넌트를 정의할 수도 있음.


class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}


React의 관점에서 볼 때, 위 두 가지 유형의 컴포넌트는 동일함.

 

6-2. 컴포넌트 렌더링

이전까지는 DOM 태그만을 사용해 React 엘리먼트를 나타냈으나,

React 엘리먼트는 '사용자 정의 컴포넌트'로도 나타낼 수 있음.


const element = <Welcome name="Sara" />;

React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면

JSX attribute와 자식을 해당 컴포넌트에 단일 객체로 전달함.

이 객체를 “props”라고 함.

예) 페이지에 “Hello, Sara”를 렌더링하는 예시.


function Welcome(props) {                  // Welcome 컴포넌트
  return <h1>Hello, {props.name}</h1>;   // 결과적으로 'Hello, Sara'를 반환.
}

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Welcome name="Sara" />;

root.render(element);

<Welcome name="Sara" /> 엘리먼트로 root.render()를 호출


React는 {name: 'Sara'}를 props로 하여 Welcome 컴포넌트를 호출.


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


React DOM은 <h1>Hello, Sara</h1> 엘리먼트와 일치하도록 DOM을 효율적으로 업데이트.
(컴포넌트의 이름은 항상 대문자로 시작.)

React는 소문자로 시작하는 컴포넌트를 DOM 태그로 처리함.

예를 들어 <div />는 HTML div 태그를 나타내지만,

<Welcome />은 컴포넌트를 나타내며 범위 안에 Welcome이 있어야 함.

 

6-3. 컴포넌트 합성

컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있음. 

이는 모든 세부 단계에서 동일한 추상 컴포넌트를 사용할 수 있음을 의미.

React 앱에서는 버튼, 폼, 다이얼로그, 화면 등의 모든 것들이 흔히 컴포넌트로 표현됨.

예) '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>
  );
}


일반적으로 새 React 앱은 최상위에 단일 App 컴포넌트를 가지고 있음. 

하지만 기존 앱에 React를 통합하는 경우, 작은 컴포넌트부터 올라가면서 점진적으로 작업해야 할 수 있음.

 


6-4. 컴포넌트 추출

컴포넌트를 여러 개의 작은 컴포넌트로 나눌 수도 있음.

 

예)  author(객체), text(문자열) 및 date(날짜)를 props로 받은 소셜 미디어 웹 사이트의 comment 컴포넌트.


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>
  );
}


이 컴포넌트는 구성요소들이 모두 중첩 구조로 이루어져 있어서 변경하기 어려울 수 있으며,

각 구성요소를 개별적으로 재사용하기도 힘듦.

이 컴포넌트를 작은 컴포넌트로 추출할 예정.

예) comment 컴포넌트에서 Avatar 컴포넌트를 추출하는 예제


function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

기존의 comment 컴포넌트에서의 props와 동일한 명칭일 필요는 없음.

→ 해당 예제에서는 props의 이름을 author에서 user로 변경해봄.

props의 이름은 사용될 context가 아닌 '컴포넌트 자체의 관점'에서 짓는 것을 추천.


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>
  );

}

예) comment 컴포넌트에서 사용자의 이름을 렌더링하는 userInfo 컴포넌트만 추출하는 예제

<userInfo 부분>

function UserInfo(props) {

  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );

}
<나머지 부분>

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>
  );
}


추출된 컴포넌트는 재사용이 가능기 때문에 더 큰 앱에서 작업할 때 유용함.

UI 일부가 여러 번 사용되거나 (Button, Panel, Avatar),

UI 일부가 자체적으로 복잡한 (App, FeedStory, Comment) 경우에는

별도의 컴포넌트를 만들어 두는 것을 추천.

 

6-5. props의 특징

읽기 전용이므로, 컴포넌트의 자체 props를 수정할 수 없음.


function sum(a, b) {
  return a + b;
}

위와 같은 함수들은 항상 동일한 입력값에 대해 동일한 결과를 반환하기 때문에 '순수 함수'라고 함. 
반면, 다음 함수는 입력값을 변경하기 때문에 순수 함수가 아님.


function withdraw(account, amount) {
  account.total -= amount;
}

모든 React 컴포넌트는 props를 다룰 때 반드시 순수 함수처럼 동작해야 함.
→ 입력값이 동일해야 함.

 

7. state와 생명주기


const root = ReactDOM.createRoot(document.getElementById('root'));
  
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);   // 렌더링 된 출력값을 변경할 목적으로 호출
}

setInterval(tick, 1000);


위의 예제에서 Clock 컴포넌트를 완전히 재사용하고 캡슐화하는 방법과,

스스로 타이머를 설정 및 매 초마다 업데이트하는 기능을 부여할 예정.
시계가 생긴 것에 따라 '캡슐화'하는 것으로 시작할 수 있음.


const root = ReactDOM.createRoot(document.getElementById('root'));

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  root.render(<Clock date={new Date()} />);
}

setInterval(tick, 1000);

위의 코드에는 Clock이 타이머를 설정하고 매초 UI를 업데이트하는 것이 누락되어 있음.
Clock을 스스로 업데이트하도록 하기 위해서는
root.render(<Clock />); 코드 내부에 “state”를 추가해야 함.

State는 props와 유사하지만, 비공개이며 컴포넌트에 의해 완전히 제어된다는 차이가 있음.

 

7-1. 함수에서 클래스로 변환

다섯 단계로 Clock과 같은 함수 컴포넌트를 클래스로 변환할 수 있음.

    1) React.Component를 확장하는 동일한 이름의 class 생성.
    2) render()라고 불리는 빈 메서드를 추가.
    3) 함수의 내용을 render() 메서드 안으로 옮김.
    4) render() 내용 안에 있는 props를 this.props로 변경.
    5) 남아있는 빈 함수 선언을 삭제.

 

수정 내용에 따라, Clock은 이제 함수가 아닌 클래스로 정의됨.


class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}


render 메서드는 업데이트가 발생할 때마다 호출되지만, 

같은 DOM 노드로 <Clock />을 렌더링하는 경우 Clock 클래스의 단일 인스턴스만 사용됨. 

이것은 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 해줌.

 

7-2. 클래스에 로컬 State 추가하기

세 단계에 걸쳐서 date를 props에서 state로 이동할 수 있음.

    1) render() 메서드의 this.props.date를 this.state.date로 변경.


class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

    2) 초기 this.state를 지정하는 class constructor를 추가.


class Clock extends React.Component {
  constructor(props) {
    super(props);           // 클래스 컴포넌트는 항상 props로 기본 constructor를 호출해야 함.
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

    3) <Clock /> 요소에서 date prop을 삭제.


root.render(<Clock />);

(타이머 코드는 나중에 다시 컴포넌트로 추가할 예정.)

결과 코드


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>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

 

7-3. 생명주기 메서드 추가

'Clock이 스스로 타이머를 설정하고 매초 UI를 업데이트하는 기능'을 부여.


일반적으로, 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 것이 중요.

Clock이 처음 DOM에 '렌더링 될 때마다 타이머를 설정'할 예정이며, “마운팅”이라고 함.
또한 Clock에 의해 생성된 DOM이 '삭제될 때마다 타이머를 해제'할 예정이며, “언마운팅”이라고 함.

컴포넌트 클래스에서 특정 메서드를 선언 → 컴포넌트가 마운트/언마운트 될 때 일부 코드를 작동할 수 있음.


class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {  // 컴포넌트 출력물이 DOM에 렌더링 된 후 실행. 이 위치가 타이머를 설정하기에 좋음.
  }

  componentWillUnmount() {
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

위의 코드와 같은 메서드들을 “생명주기 메서드”라 부름.

위의 코드에서 componentDidMount() 메서드에 추가해 줄 내용.


  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

this (this.timerID)에서 어떻게 타이머 ID를 제대로 저장하는지를 주의해서 확인하는 것이 필요.
this.props가 스스로 설정되고 this.state가 특수한 의미가 있지만

타이머 ID와 같이 데이터 흐름 안에 포함되지 않는 어떤 항목을 보관해야 하는 경우,

자유롭게 클래스에 수동으로 부가적인 필드를 추가할 수 있음.

위의 코드에서 componentWillUnmount() 메서드에 추가해 줄 내용.


  componentWillUnmount() {
    clearInterval(this.timerID);
  }


결과 코드


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() {               // Clock 컴포넌트가 매초 작동하도록 하는 tick()이라는 메서드를 추가.
    this.setState({             // 컴포넌트 로컬 state를 업데이트하기 위한 목적으로 this.setState()를 사용
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<Clock />);

전체적인 진행 과정
    1) <Clock />가 root.render()로 전달되었을 때 React는 Clock 컴포넌트의 constructor를 호출.

    2) Clock이 현재 시각을 표시해야 하기 때문에 현재 시각이 포함된 객체로 this.state를 초기화.

        (추후 이 state를 업데이트할 예정).
    3) React는 Clock 컴포넌트의 render() 메서드를 호출.

        (이를 통해 React는 화면에 표시되어야 할 내용을 알 수 있음).

    4) Clock의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트.
    5) Clock 출력값이 DOM에 삽입되면, React는 componentDidMount() 생명주기 메서드를 호출.

    6) 매 초 컴포넌트의 tick() 메서드 호출 목적의  타이머를 설정하도록 브라우저에 요청. 
    7) 매초 브라우저가 tick() 메서드를 호출.

    8) setState()에 현재 시각을 포함하는 객체를 호출하며 UI 업데이트를 진행.

    9) React는 state가 변경된 것을 인지하고 화면에 표시될 내용의 확인 목적으로 render() 메서드를 호출.

        (이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함함.)       

   10) React는 이에 따라 DOM을 업데이트.
   11) Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면

         React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출.

   12) 정상적으로 작동

 


7-4. State 주의 사항

1) 직접 State를 수정하는 것은 지양


this.state.comment = 'Hello'; // 잘못된 코드. 이 코드는 컴포넌트를 다시 렌더링하지 않음.

this.setState({comment: 'Hello'}); // 올바른 코드. setState()를 사용.

constructor에서만 this.state를 지정할 수 있음.

 

 

2) State 업데이트는 비동기적일 수 있음.



this.setState({
  counter: this.state.counter + this.props.increment,  // 잘못된 코드. 결과값이 counter의 업데이트에 실패
});


this.setState((state, props) => ({              // 이전 state를 첫 번째 인자로, 업데이트가 적용된 시점의 props를 두 번째 인자로 함.
  counter: state.counter + props.increment
}));  // 올바른 코드. 객체보다는 함수를 인자로 사용하는 다른 형태의 setState()를 사용.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있음.
→ this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에

     다음 state를 계산할 때 해당 값에 의존해서는 안됨.

 


3) State 업데이트는 병합됨.
setState()를 호출할 때 React는 제공한 객체를 현재 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
      });
    });
  }

병합은 얕게 이루어지기 때문에 this.setState({comments})는 this.state.posts에 영향을 주지 않음.

 


4) 데이터는 top-down으로 흐름

부모/자식 컴포넌트 모두 특정 컴포넌트의 상태에 대한 유/무 여부 및 함수/클래스 정의 여부를 알 수 없음.
state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없음.
이러한 이유로 state는 '로컬' 또는 '캡슐화'라고 불림.


컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있음.


<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고

이것이 Clock의 state/props 및 수동으로 입력한 것인지의 여부를 알 수 없음.


function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

일반적으로 이를 “하향식(top-down)” 또는 “단방향식” 데이터 흐름이라고 함.

모든 state는 항상 특정 컴포넌트가 소유하고 있으며

그 state로부터 파생된 UI/데이터는 오직 트리구조에서 본인의 '아래'에 있는 컴포넌트에만 영향을 미침.

트리구조가 props들의 폭포라면,

각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 수원(water source)이라고 할 수 있음.


function App() {
  return (       // App에서 렌더링하는 세 개의 Clock을 반환.
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

위의 코드에서 모든 컴포넌트가 완전히 독립적이라는 것을 알 수 있음.

각 Clock은 자신만의 타이머를 설정하고 독립적으로 업데이트함.

React 앱에서 컴포넌트의 유상태/무상태는 시간이 지남에 따라 변경될 수 있는 구현 세부 사항으로 간주.

유상태 안에서 무상태를 사용할 수 있으며, 그 반대 경우도 가능함.

 

8. 이벤트 처리하기

React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트처리하는 방식과 유사함.

 

몇 가지 문법의 차이는 다음과 같음.
    1) React의 이벤트는 소문자 대신 캐멀 케이스(camelCase)를 사용.
    2) JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달.
        예)


<button onclick="activateLasers()">   // HTML에서의 경우
  Activate Lasers
</button>

<button onClick={activateLasers}>  // React에서의 경우
  Activate Lasers
</button>

    3) React에서는 false를 반환해도 기본 동작을 방지할 수 없음.

        → 반드시 preventDefault를 명시적으로 호출해야 함.

        예)


<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>    // HTML에서 폼을 제출할 때 가지고 있는 기본 동작을 방지할 목적으로 작성
</form>

function Form() {       // React에서 동일 목적으로 작성.
  function handleSubmit(e) {    // e는 합성 이벤트.
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}


DOM 엘리먼트가 생성된 후 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없음.

→ 대신, 엘리먼트가 처음 렌더링될 때 리스너를 제공해야 함.

ES6 클래스를 사용하여 컴포넌트를 정의할 때, 

일반적인 패턴은 이벤트 핸들러를 클래스의 메서드로 만드는 것.

예) 사용자가 “ON”과 “OFF” 상태를 토글 할 수 있는 버튼을 렌더링하는 Toggle 컴포넌트를 생성.


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>
    );
  }
}

JSX 콜백 안에서 this의 의미에 대해 주의

 

JavaScript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않음.

this.handleClick을 바인딩하지 않고 onClick에 전달하였다면,

함수가 실제 호출될 때 this는 undefined가 됨.


이는 React만의 특수한 동작이 아니며, JavaScript에서 함수가 작동하는 방식의 일부임.

일반적으로 onClick={this.handleClick}과 같이 뒤에 ()를 사용하지 않고 메서드를 참조할 경우,

해당 메서드를 바인딩 해야 함.

bind를 호출하는 것이 불편하다면, 이를 해결할 수 있는 방법으로

콜백을 올바르게 바인딩하기 위해 '퍼블릭 클래스 필드' 문법을 활용할 수 있음.

예)



class LoggingButton extends React.Component {     // `this`가 handleClick 내에서 바인딩되도록 함.
  handleClick = () => {
    console.log('this is:', this);
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

클래스 필드 문법을 사용하고 있지 않다면, 콜백에 화살표 함수를 사용하는 방법도 있음.


class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {                    // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 함.
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

이 문법의 문제점은 LoggingButton이 렌더링될 때마다 다른 콜백이 생성된다는 점임.

만약 콜백이 하위 컴포넌트에 props로서 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행함.

이러한 종류의 성능 문제를 피하려면, 생성자 안에서 바인딩하거나 클래스 필드 문법의 사용을 추천함.

 

8-1. 이벤트 핸들러에 인자 전달

루프 내부에서는 이벤트 핸들러에 추가적인 매개변수를 전달하는 것이 일반적임.

예를 들어, id가 행의 ID일 경우 다음 코드가 모두 작동함.



<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>  // 화살표 함수를 사용

=

<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>  // Function.prototype.bind를 사용

둘 다 React 이벤트를 나타내는 e 인자가 ID 뒤에 두 번째 인자로 전달됨.

(화살표 함수는 명시적으로 인자를 전달해야 하지만, bind는 인자가 자동으로 전달되어 명시적일 필요 없음.)

 

9. 조건부 렌더링

React에서는 원하는 동작을 캡슐화하는 컴포넌트를 만들 수 있음.

이런 경우 상황에 따라 컴포넌트 중 몇 개만을 렌더링할 수 있음.

React에서 조건부 렌더링은 JavaScript에서의 조건 처리와 동일하게 동작함.

JavaScript 연산자(if문, 조건부 연산자 등)를 현재 상태를 나타내는 엘리먼트를 만드는 데에 사용 시,

React는 현재 상태에 맞게 UI를 업데이트함.

 

예) 2개의 인사말을 반환하는 컴포넌트 2개를 생성


function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}

위의 코드와 관련하여 로그인 상태에 맞게 컴포넌트 중 하나를 보여주는 Greeting 컴포넌트를 생성.

예)  isLoggedIn prop에 따라서 다른 인사말을 렌더링하는 예제.


function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;

  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

const root = ReactDOM.createRoot(document.getElementById('root')); 

// Try changing to isLoggedIn={true}:
root.render(<Greeting isLoggedIn={false} />);


9-1. 엘리먼트 변수

엘리먼트를 저장하기 위해 변수를 사용할 수 있음.

출력의 다른 부분은 변하지 않은 채로 컴포넌트의 일부를 조건부로 렌더링 할 수 있음.

예) 로그아웃과 로그인 버튼을 나타내는 두 컴포넌트 생성.


function LoginButton(props) {
  return (
    <button onClick={props.onClick}>
      Login
    </button>
  );
}

function LogoutButton(props) {
  return (
    <button onClick={props.onClick}>
      Logout
    </button>
  );
}


예) LoginControl이라는 유상태 컴포넌트를 생성하여

상태에 따라 <LoginButton />이나 <LogoutButton />, 및 이전 예시의 </Greeting />까지 렌더링하는 예제


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;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }

    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<LoginControl />);

 

만약 더 짧은 구문을 사용하고 싶은 경우,

아래의 두 방법과 같이 element의 여러 조건을 JSX 안에서 인라인(inline)으로 처리하는 방법을 사용함.

1) 논리 && 연산자로 If를 인라인으로 표현
JSX 안에는 중괄호를 이용해서 표현식을 포함 할 수 있음.

그 안에 JavaScript의 논리 연산자 '&&'를 사용하면 쉽게 엘리먼트를 조건부로 넣을 수 있음.


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'];

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<Mailbox unreadMessages={messages} />);

JavaScript에서

true && expression은 항상 expression으로 평가되고,

false && expression은 항상 false로 평가됨.

따라서 && 뒤의 엘리먼트는 조건이 true일때 출력. 조건이 false라면 React는 무시하고 건너뜀.
그러나 falsy 표현식을 반환하면 && 뒤에 있는 표현식은 건너뛰지만 falsy 표현식이 반환된다는 것을 주의.

예) <div>0</div>이 render 메서드에서 반환되는 예제.


render() {
  const count = 0;
  return (
    <div>
      {count && <h1>Messages: {count}</h1>}
    </div>
  );
}



2) 조건부 연산자로 If-Else구문 인라인으로 표현
조건부 연산자인 condition ? true: false를 사용.

예) 짧은 구문을 조건부로 렌더링하는 예제.


render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}

가독성은 좀 떨어지지만, 더 큰 표현식에도 이 구문을 사용할 수 있음.


render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn
        ? <LogoutButton onClick={this.handleLogoutClick} />
        : <LoginButton onClick={this.handleLoginClick} />
      }
    </div>
  );
}


9-2. 컴포넌트가 렌더링하는 것을 방지

가끔 다른 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶은 경우가 있음.

이때는 렌더링 결과를 출력하는 대신 null을 반환하면 해결할 수 있음.

예) <WarningBanner />가 warn prop의 값에 의해서 렌더링되는 예제.

(prop이 false라면 컴포넌트는 렌더링하지 않게 됨.)


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(state => ({
      showWarning: !state.showWarning
    }));
  }

  render() {
    return (
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.handleToggleClick}>
          {this.state.showWarning ? 'Hide' : 'Show'}
        </button>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<Page />);

컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않음.

그 예로, 위에서의 componentDidUpdate는 계속 호출됨.

 

10. 리스트와 Key

JavaScript에서 리스트를 변환하는 방법.

예)  map()함수를 이용하여 numbers 배열의 값을 두배로 만든 후,

map()에서 반환하는 새 배열을 doubled 변수에 할당하고 로그를 확인하는 예제.


const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);

console.log(doubled);  // 콘솔에 [2, 4, 6, 8, 10]을 출력

React에서 배열을 엘리먼트 리스트로 만드는 방식은 이와 거의 동일.

 

10-1. 여러개의 컴포넌트 렌더링

엘리먼트 모음을 만들고 중괄호( {} )를 이용하여 JSX에 포함 시킬 수 있음.

 

예) <ul> 엘리먼트 안에 전체 listItems 배열을 포함하는 예제.
      (map() 함수를 사용하여 numbers 배열을 반복 실행하여 1부터 5까지의 숫자로 이루어진 리스트를 출력.

      각 항목에 대해 <li> 엘리먼트를 반환하고 엘리먼트 배열의 결과를 listItems에 저장.)


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

그러면 <ul> 엘리먼트 안에 전체 listItems 배열을 포함할 수 있음.


<ul>{listItems}</ul>


10-2. 기본 리스트 컴포넌트

일반적으로 컴포넌트 안에서 리스트를 렌더링함.

예) 위의 예시를 numbers 배열을 받아 '순서 없는 엘리먼트 리스트를 출력'하는 컴포넌트로 리팩토링.


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];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

이 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시됨.

“key”는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 attribute임.

 

예) numbers.map() 안에서 리스트의 각 항목에 key를 할당 → 키 누락 문제의 해결 가능.


function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

 

10-3. Key

Key는 React가 특정 항목의 변경/추가/삭제 여부를 식별하는 것을 도움.

엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 함.


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

Key를 선택하는 방법으로 고유하게 식별할 수 있는 문자열을 사용하는 것을 추천.

대부분의 경우 데이터의 ID를 key로 사용.


const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

렌더링한 항목에 대한 안정적인 ID가 없다면 항목의 인덱스를 key로 사용할 수 있음.


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

그러나, 항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 비추천함.

이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있음.

리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용함.

 

10-4. Key로 컴포넌트 추출

키는 주변 배열의 context에서만 의미가 있음.

만약 ListItem 컴포넌트를 추출 한 경우,

ListItem 안에 있는 <li> 엘리먼트가 아니라

배열의 <ListItem /> 엘리먼트가 key를 가져야 함.

예) 잘못된 Key의 사용법


function ListItem(props) {
  const value = props.value;
  return (
    // 틀렸습니다! 여기에는 key를 지정할 필요가 없습니다.
    <li key={value.toString()}>
      {value}
    </li>
  );
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 틀렸습니다! 여기에 key를 지정해야 합니다.
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}


예) 올바른 Key 사용법


function ListItem(props) {
  // 맞습니다! 여기에는 key를 지정할 필요가 없습니다.
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 맞습니다! 배열 안에 key를 지정해야 합니다.
    <ListItem key={number.toString()} value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

map() 함수 내부에 있는 엘리먼트에 key를 넣어 주는 것을 추천함.

Key는 형제 사이에서만 고유한 값이어야 하며, 전체 범위에서 고유할 필요는 없음.

두 개의 다른 배열을 만들 때 동일한 key를 사용할 수 있음.


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.'}
];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);

React에서 key는 컴포넌트로 전달하지는 않음.

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


const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

위 예시에서 Post 컴포넌트는 props.id를 읽을 수 있지만, props.key는 읽을 수 없음.

 

10-5. JSX에 map()을 포함

위 예시에서 별도의 listItems 변수를 선언하고 이를 JSX에 포함함.


function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <ListItem key={number.toString()}
              value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

JSX를 사용하면 중괄호 안에 모든 표현식을 포함 시킬 수 있음.

→ map() 함수의 결과를 인라인(inline)으로 처리할 수 있음.


function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}

위의 코드는 가독성이 좋으나 자주 사용하는 것은 비추천함.

가독성을 위해 변수로 추출해야 할지, 인라인으로 넣을지는 개발자가직접 판단해야 함. 

map() 함수가 너무 중첩된다 → 컴포넌트로 추출 하는 것을 추천.