1. React 리서치는 어떻게 할건데?

React 공식사이트에서 제공하는 ‘문서’와 자습서’를 탐독하고 실습하여 최대한 속성으로 React의 기본적인 사항들을 마스터하도록 한다.

[React 공식사이트 안내서 링크]

[React 공식사이트 자습서 링크]

2. React 란?

  • 클라이언트 사이드의 JS 라이브러리 제공과 웹 개발에 필요한 현대적이고 반응적인 사용자 인터페이스를 구축하기 위한 모든 것을 지원하는 프레임워크이다.
  • 선언적, 구성 요소 중심의 접근방식을 사용한다.
  • 리액트는 싱글 페이지 어플리케이션(SPA)이나 모바일 어플리케이션 개발에 사용한다.
  • 리액트는 페이스북의 소프트웨어 엔지니어 Jordan Walke가 개발하였다.

싱글 페이지 어플리케이션(SPA: Single-Page-Application)

SPA 방식은 서버가 하나의 HTML 페이지만 보낸 후, React가 UI를 인계받아 제어하는 방식이다.

3. 새로운 React 프로젝트 만들기

  • 순서1. 새로운 프로젝트 생성하기
npx create-react-app my-app
  • 순서2. 프로젝트 경로 접근하기
cd my-app
  • 순서3. (프로젝트 경로에 접근한 상태) 프로젝트 열기
npm start
  • 순서4. src/ 폴더에 있는 모든 파일 삭제
del *
  • 순서5. 상위폴더로 돌아가기
cd ..
  • 순서6. src/ 폴더에 index.css 파일 생성하기
  • 순서7. src/ 폴더에 index.js 파일 생성하기
  • 순서8. 위에서 생성한 index.js 의 상단에 아래 세 줄을 추가하기
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

4. React 엘리먼트: JSX 표현식

JSX는 JavaScript 코드 안에서 UI 관련 작업을 함께 하는 것을 말하는데, 본질적으로 렌더링 로직이 UI 로직(이벤트가 처리되는 방식, 시간에 따라 state가 변하는 방식, 화면에 표시하기 위해 데이터가 준비되는 방식 등)과 연결된다.

JSX는 if, for 등을 사용하거나, 변수를 할당하고, 인수를 받아, 함수를 반환할 수도 있다.

[예시 1 - JavaScript 함수 호출의 결과인 formatName(user)를 h1 엘리먼트에 포함한 경우]

const user = () => {
    firstName = 'DK';
    lastName = ' Lee';
}


const formatName = (user) => {
    return user.firstName + ' ' + user.lastName;
}

const element = (
    <h1>
        Hello, {formatName(user)}!
    </h1>
);

[예시 2 - JSX 표현식이 반환값으로 사용된 예]

const getGreeting = (user) => {
    if (user) {
        return <h1>Hello, {formatName(user)}!</h1>;
    }
    return <h1>Hello, Stranger.</h1>
}

5. React 엘리먼트: JSX 자식 요소 정의

JSX 태그는 자식을 포함할 수 있는데, 자식이 없는 태그인 경우는 반드시 ‘/’로 닫아주어야 한다.

[예시 1 - 자식이 포함된 예]

const element = (
	<div>
		<h1>Hello!</h1>
		<h2>Good to see you here.</h2>
	</div>
);

[예시 2 - 더 이상의 요소의 태그가 없는 경우]

const element = <img src={user.avatarUrl} />;

6. React 엘리먼트: JSX의 객체 표현

React 엘리먼트는 React 앱의 가장 작은 단위이며, 화면이 어떻게 나타나야하는지를 담고 있다.

다음 두 예시는 동일한 예시이다.

const element = (
    <h1 className="greeting">
        Hello, world!
    </h1>
);
const element = React.createElement(
    'h1',
    {className: 'greeting'},
    'Hello, world!'
);

여기서 React.createElement() 는 버그가 없는 코드를 작성하는데 도움이 되도록 몇 가지 검사를 수행하며, 기본적으로 다음과 같은 객체를 생성한다.

const element = {
	type: 'h1',
	props: {
		className: 'greeting',
		children: 'Hello, world!'
	}
};

element 는 변수처럼 보이지만, 안에 화면에서 보고 싶은 것을 JSX 표현식으로 나타낸 ‘React 엘리먼트’라 한다.

이렇게 만들어진 React 엘리먼트를 DOM에 렌더링하면 화면에서 볼 수 있다.

여기서 주의할 점은, 엘리먼트와 컴포넌트를 혼동해서는 안된다는 점이다.

7. React 엘리먼트: 엘리먼트 렌더링

React는 모든 엘리먼트를 React DOM에서 관리하는데, 아래와 같은 요소를 ‘루트(root)’ DOM 노드라고 한다.

<div id="root"></div>

[예시 - element를 root DOM에 렌더링 한 예]

const root = ReactDOM.createRoot(
    document.getElementById('root')
);
const element = <h1>Hello, world!</h1>;
root.render(element);

8. React 엘리먼트: 렌더링 된 엘리먼트 업데이트하기

React 엘리먼트는 불변객체로서, 엘리먼트를 생성한 이후에는 자식이나 속성을 변경할 수 없다.

하지만, UI를 업데이트할 수 있는 유일한 방법이 있는데 아래와 같이 특정 함수를 호출할 때마다 새로운 엘리먼트를 생성하여 root.render()로 전달하는 방법이 있다.

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

let tick = () => {
    // 새로운 엘리먼트를 생성하여 불변객체를 DOM에 렌더링할 수 있음
    const element = (
        <div>  
            <h1>Hello!</h1>
            <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
    );
    root.render(element);
}

setInterval(tick, 1000);

9. React 엘리먼트: 똑똑한 React DOM

React DOM은 해당 엘리먼트와 그 자식 엘리먼트를 이전의 엘리먼트와 비교하여, 바뀌어있는 속성만을 업데이트한다.

매초 전체 UI를 다시 그리도록 엘리먼트 로직을 작성했지만, React DOM은 내용이 변경된 텍스트 노드만 업데이트한다.

10. React 컴포넌트: 함수 컴포넌트와 클래스 컴포넌트

개념적으로 컴포넌트는 Javascript 함수와 유사하다. ‘props’라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술한 React 엘리먼트를 반환하는 구조이다.

[예시 1 - 함수 컴포넌트]

const Welcome = (props) => {
    return <h1>Hello, {props}</h1>;
}

root.render(Welcome('이동규'));

[예시 2 - 클래스 컴포넌트]

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

React의 관점에서 볼 때, 위 두 가지 유형의 컴포넌트는 동일하다. (위 예제에서 클래스 컴포넌트의 constructor는 생략되어 있다.)

props를 명시적으로 표현하기 ← 송차장님 솔루션

function에 인수로 props.xxx 를 사용하기 보다는 아래 예시 코드와 같이 명시적으로 인수를 설정하도록 하자.

// props 에 name 속성을 명시적으로 추가
const ShowName = ({name = ''}) => {
    return (
    <div>
      <h1>
        {name}
      </h1>
    </div>
  );
}

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

11. *함수 컴포넌트: 렌더링

엘리먼트의 이름을 나타낼 때, 컴포넌트의 이름을 사용할 수 있다.

많은 예제에서 element를 렌더링할 때, 컴포넌트의 이름으로 엘리먼트 이름을 사용하니 숙달하도록 하자.

그리고 컴포넌트의 이름을 소문자로 사용하면, 태그로 처리하여 정상적으로 렌더링이 되지 않기에 반드시 대문자로 컴포넌트의 이름을 정해야한다.

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

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

// 컴포넌트의 이름이 엘리먼트 이름으로 사용
const element = <Welcome name="Sara" />;


// 에러
const element = <welcome name="Sara" />;

root.render(element);

12. *함수 컴포넌트: 합성

컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있다. 쉽게 말하면, JS의 nest 함수와 비슷한 구조라 보면 된다. 단, React 컴포넌트는 반드시 반환값으로 다른 컴포넌트를 지정하여 화면에 반복 출력되게 해야 합성에 의미가 있다.

const Count = (props) => {
    return <h1>This is {props.cnt}</h1>
};

const CountList = () => {
    return (
        <div>
            <Count cnt="1" />
            <Count cnt="2" />
            <Count cnt="3" />
        </div>
    );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <CountList />;

// This is 1
// This is 2
// This is 3
root.render(element);

13. *함수 컴포넌트: 분리, 추출

컴포넌트를 엘리먼트를 기준으로 추출하면, (1)추출된 컴포넌트들이 재사용, (2)클린코드가 가능해진다.

컴포넌트를 추출할 때에는 가장 안쪽에 있는 컴포넌트부터 시작하도록한다. 아래 중첩 구조로 이루어져 추출하기 어려운 예시 코드를 통해 연습해보도록하자.

아래 코드를 분석해보면, Comment 의 자식으로 UserInfo, comment-text, comment-date가 있고, UserInfo의 자식으로 Avart가 있다.

먼저, UserInfo를 분리하기 전에 가장 안쪽에 있는 Avatar부터 분리하도록 한다.

const Comment = (props) => {
    return (
        <div className="Comment">
            <div className="UserInfo">
                <img className="Avatar"  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>
    );
}

함수 컴포넌트를 구성할 때, 함수 컴포넌트의 기본 구조를 반드시 기억하고 분류하도록 하자. 함수 컴포넌트의 기본 구조는 (1) props를 인수로 받고 (2) 엘리먼트를 반환하는 구조이다.

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

코드가 조금 단순해졌다. 다음으로 UserInfo 컴포넌트를 추출하도록 하자. UserInfo는 자식으로 Avatar 렌더링, UserInfo-name을 갖고 있다.

const Comment = (props) => {
    return (
        <div className="Comment">
            <div className="UserInfo"> ← UserIfno 분리
                <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>
    );
}

아래와 같이 UserInfo를 추출하였다.

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

추출한 UserInfo를 Comment 컴포넌트에 렌더링하였더니, 처음 코드보다 훨씬 깔끔해졌다.

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

14. *함수 컴포넌트: props는 읽기 전용이다

React의 함수 컴포넌트나 클래스 컴포넌트는 입력값 props를 수정해서는 안된다.

예를 들어 아래와 같이 ‘순수함수’ 즉, 입력값을 바꾸지 않고 반환하는 함수 구조로 작성해야 한다.

const Add = (props) => {
    return (
        <div className="add">
            {props.a + props.b}
        </div>
    );
}

반면에 아래와 같이 자신의 입력값을 변경하는 함수는 순수함수가 아니다.

const Add = (props) => {
    return (
        <div className="add">
            {props.a += props.b} ← 에러, cannot assign
        </div>
    );
}

따라서. 모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수함수 구조가 되게끔 엄격히 작성하도록 하자.

15. *클래스 컴포넌트: 특징

아래 순서로 Clock 함수 컴포넌트를 Clock 클래스 컴포넌트로 변경할 수 있다.

순서1. React.Component를 확장하는 ES6 Class를 생성한다.

순서2. render() 라고 불리는 빈 메서드를 추가한다.

순서3. 함수의 내용을 render() 메서드 안으로 옮긴다.

순서4. render() 메서드 안에 props를 this.props로 변경한다.

// Clock 함수 컴포넌트
const Clock = (props) => {
    return (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {props.date.toLocaleTimeString()}.</h2>
        </div>
    );
}

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

16. *클래스 컴포넌트: State 추가하기

아래 순서로 Clock 클래스에 State를 추가할 수 있다.

순서1. props를 state로 변경한다.

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

순서2. this.state를 지정하는 constructor를 추가한다.

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

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

여기서 어떻게 props를 constructor에 전달하는지 잘봐야한다.

super() 함수가 먼저 호출되어야 ‘this’ 키워드를 사용할 수 있다. 그렇지 않을 경우 참조 오류가 발생한다. 여기서, super 키워드는 부모클래스(React.Component)의 생성자 함수를 호출하여 props를 넘기는 역할을 한다.

[super 키워드 참조 링크]

constructor(props) { 
	super(props);  super 키워드로 부모클래스 생성자를 호출하여 props를 넘김
	this.state = {date: new Date()};  super() 함수가 호출되어야 this.state를 문제 없이 사용할  있음 
}

순서 3. 아래와 같이 클래스 컴포넌트 이름으로 렌더링한다. (단, 아직은 1초 간격으로 업데이트되지는 않는다.)

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 />);  Clock 클래스 컴포넌트 렌더링

17. *클래스 컴포넌트: setState와 생명주기 메서드를 클래스에 추가하기

많은 컴포넌트가 있는 어플리케이션에서 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 것이 중요하다.

클래스 컴포넌트(Clock)가 처음 DOM에 렌더링 될 때마다 타이머를 설정해야하는데, 이것을 React에서 ‘마운팅’이라고 한다.

반대로, 생성된 DOM이 삭제될 때마다 타이머를 해제해야하는데, 이것을 ‘언마운팅’이라고 한다. 이러한 메서드들을 ‘생명주기 메서드’라고 한다.

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

    componentDidMount() {  생명주기 메서드
	
    }

    componentWillUnmount() {  생명주기 메서드
        
    }

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

componentDidMount() 메서드는 컴포넌트 출력물이 DOM에 렌더링된 후에 실행된다. 이 위치에 타이머를 설정한다.

this.timerID 에 주목해야 하는데, React에 의해 자동으로 설정되는 this.props와는 달리 클래스에 직접 부가적인 필드를 추가할 수도 있다.

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

componentWillUnmount() 메서드 안에 타이머를 초기화한다.

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

마지막으로 Clock 컴포넌트가 매초 작동하도록 하는 tick() 메소드를 구현한다.

이때, 컴포넌트 state를 업데이트하기 위해 this.setState()를 사용한다.

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({	 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 />);

위에 코드는 아래 방식으로 작동한다.

  1. Clock 클래스 컴포넌트가 root.render()로 전달되었을 때, React는 Clock 컴포넌트의 constructor를 호출한다.이 때 현재 시각을 표시하기 위해, 현재 시각이 포함된 객체를 this.state에 초기화한다. 그리고 이 state 는 업데이트할 것이다.
  2. React는 Clock 컴포넌트의 render() 메서드를 호출한다. 이를 통해 React는 화면에 표시되어야 할 내용을 알게 된다. 그 다음 React는 Clock의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트한다.
  3. Clock 출력값이 DOM에 삽입되면, React는 ComponentDidMount() 생명주기 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 매초 컴포넌트의 tick() 메소드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.
  4. 매초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setState() 에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다. setState() 호출 덕분에 React는 state가 변경된 것을 확인하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함한다. React는 이에 따라 DOM을 업데이트한다.
  5. Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출한다.

18. *클래스 컴포넌트: State 올바르게 사용하기

setState() 에 대해서 알아야만 하는 세 가지가 있다.

(1) State를 직접 수정하지 말고, setState()를 사용하여 수정한다.

예를 들어, 아래와 같은 포현식은 컴포넌트를 렌더링하지 않는다.

this.state.comment = 'Hello';

대신에 setState()를 사용한다.

this.setState({comment: 'Hello'});

(2) this.sate를 지정할 수 있는 유일한 공간은 constructor 뿐이다.

(3) State 업데이트는 비동기적일 수 있다.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안된다.

예를 들어, 다음 코드는 카운터 업데이트에 실패할 수 있다.

this.setState({
	counter: this.state.counter + this.props.increment
});

이를 수정하기 위해 객체보다는 함수를 인자로 사용하여 state를 첫 번째 인자로 받아들이고, 업데이트가 적용된 시점에 props를 두번째 인자로 받아들이자.

this.setSate((state, props) => ({
	counter: state.counter + props.increment
}));

(4) State 업데이트는 독립적으로 업데이트할 수 있지만, 병합된다.

class example extends React.Component {
    // state는 다양한 형태의 데이터를 property로 
    constructor(props) {
        super(props);
        this.state = {
            posts: [],
            comments: [],
        };
    }

    // state는 독립적으로 업데이트할 수도 있다
    componentDidMount() {
        fetchPosts().then(response => {
            this.setState(
                {posts: response.posts}
            );
        });
        
        fetchComments().then(response => {
            this.setState(
                {comments: response.comments}
            );
        });
    }
}

(5) 데이터(props, state)는 트리구조이다.

모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 ‘아래’에 있는 컴포넌트에만 영향을 미친다.

트리구조가 props들의 폭포라고 상상하면 각 컴포넌트의 state는 임의의 점에서 만나지만 동시에 아래로 흐르는 부가적인 수원이라고 할 수 있다.

예를 들어, 아래의 세 개의 Clock은 자신만의 타이머를 설정하고 독립적으로 업데이트한다.

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

image-20220920174627811

19. **[정리] 함수 컴포넌트와 클래스 컴포넌트의 비교

함수 컴포넌트

  • state 기능 및 라이프 사이클 메서드를 사용할 수 없다.
  • useState, Hook을 이용해, 함수 컴포넌트에서도 state를 사용하거나 자식에서 부모로 값을 전달할 수 있다. (단, 함수 컴포넌트에서 useState를 사용했을 때와 클래스 컴포넌트에서 state를 사용했을 때, 작동방식이 다르다고 함)
  • 클래스형 컴포넌트보다 심플하다.
  • 클래스형 컴포넌트보다 메모리자원을 적게 사용한다.

클래스 컴포넌트

  • state, 라이프 사이클 메서드를 사용할 수 있다.
  • 메서드를 정의할 수 있다.
  • render 함수가 반드시 필요하다.

공통점

  • 호출할 때, <이름> 으로 호출할 수 있다.

20. **[정리] Props와 State의 비교

Props

  • 부모에서 자식으로 값을 전달할 때 사용
  • 수정이 불가한 ‘읽기 전용’
  • 함수 컴포넌트와 사용

State

  • 컴포넌트의 내부에서 선언되기 때문에 컴포넌트에서 개별적으로 state를 관리한다.
  • 외부 컴포넌트로 state를 전달하고 싶을 때는, props를 통해 전달한다.
  • state는 변경할 수 있지만, 직접 수정할 수 없고 setState()를 이용하여 수정할 수 있다.
  • 클래스 컴포넌트와 사용할 수 있지만, HOOK을 이용하여 함수 컴포넌트에서도 사용할 수 있다.

21. 이벤트 처리하기

  • React의 이벤트는 소문자 대신 camelCase를 사용한다.
  • JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다.

[예시 1 - 함수로 이벤트 처리하기]

function Form() {
    function handleSubmit(e) {
      e.preventDefault();
      console.log('You clicked submit.');
    }
  
    return (
      <form onSubmit={handleSubmit}>
        <button type="submit">Submit</button>
      </form>
    );
  }

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

React에서는 false를 반환해도 기본 동작을 방지할 수 없다. 반드시 preventDefault를 명시적으로 호출해야 한다.

위에서 e는 합성 이벤트이다. React는 합성 이벤트를 정의하기 때문에 브라우저 호환성에 대해 걱정할 필요가 없다.

[예시 2 - 클래스로 이벤트 처리하기]

클래스 컴포넌트에서 function 함수로 이벤트 핸들러를 정의할 때는 반드시 바인딩 해주어야 한다.

하지만, 실험적?으로 화살표 함수를 통해 바인딩을 생략할 수 있다.

화살표 함수로 바인딩을 생략하는 경우 대부분의 경우 문제가 되지 않으나, 콜백이 하위 컴포넌트에 props로 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행할 수 있는데, 이러한 종류의 성능 문제를 피하기 위해서 생성자 안에서 바인딩하거나 클래스 필드 문법을 사용하는 것을 권장한다고 한다.

class Counting extends React.Component {
    constructor(props){
        super(props);
        this.state = {number: 0};
    }

    // 여기서 count를 function 함수로 정의하면, this.count = this.count.bind(this); 로 바인딩해줘야 함
    // 그렇지 않으면 undefined 에러
    // 화살표 함수를 사용하여 바인딩 생략가능
    count = () => {
    this.setState(
        {
            number: this.state.number + 1
        }
        );
    }

    render() {
        return (
            <div>
                <button onClick={this.count}>
                    Click me!
                </button>
                <span>
                    {this.state.number}
                </span>
            </div>
            
        );
    }
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counting />);

위에 코드는 아래 방식으로 작동한다.

  1. Clock 클래스 컴포넌트가 root.render()로 전달되었을 때, React는 Clock 컴포넌트의 constructor를 호출한다.이 때 현재 시각을 표시하기 위해, 현재 시각이 포함된 객체를 this.state에 초기화한다. 그리고 이 state 는 업데이트할 것이다.
  2. React는 Clock 컴포넌트의 render() 메서드를 호출한다. 이를 통해 React는 화면에 표시되어야 할 내용을 알게 된다. 그 다음 React는 Clock의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트한다.
  3. Clock 출력값이 DOM에 삽입되면, React는 ComponentDidMount() 생명주기 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 매초 컴포넌트의 tick() 메소드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.
  4. 매초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setState() 에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다. setState() 호출 덕분에 React는 state가 변경된 것을 확인하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함한다. React는 이에 따라 DOM을 업데이트한다.
  5. Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출한다.

22. 조건문: 조건부 렌더링

React에서 조건부 렌더링은 JS 조건처리와 동일하다.

단지, 반환되는 값이 엘리먼트일 뿐이며, 조건문을 활용하여 페이지에 렌더링되는 컴포넌트들을 제어할 수 있다.

const UserGreeting = (props) => {
    return <h1>환영합니다! 로그인해주세요.</h1>
}

const GuestGreeting = (props) => {
    return <h1>로그아웃을 원하신다면, 로그아웃 버튼을 눌러주세요.</h1>
}

const Greetings = (props) => {
    const isLoggedIn = props.isLoggedIn;
    if (isLoggedIn) {
        return <UserGreeting />;
    } else {
        return <GuestGreeting />;
    }
}

23. 조건문: 엘리먼트 변수

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

const UserGreeting = (props) => {
    return <h1>환영합니다! 로그인해주세요.</h1>
}

const GuestGreeting = (props) => {
    return <h1>로그아웃을 원하신다면, 로그아웃 버튼을 눌러주세요.</h1>
}

const Greetings = (props) => {
    const isLoggedIn = props.isLoggedIn;
    if (isLoggedIn) {
        return <UserGreeting />;
    } else {
        return <GuestGreeting />;
    }
}

function LoginButton(props) {
    return (
      <button onClick={props.onClick}>
        Login
      </button>
    );
  }
  
  function LogoutButton(props) {
    return (
      <button onClick={props.onClick}>
        Logout
      </button>
    );
  }
  
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {isLoggedIn: false};
        // 이벤트 핸들러를 화살표 함수로 만들어 바인딩 생략
        // this.handleLoginClick = this.handleLoginClick.bind(this);
        // this.handleLogoutClick = this.handleLogoutClick.bind(this);
    }

    handleLoginClick = () => {
        this.setState({isLoggedIn: true});
    }
    
    handleLogoutClick = () => {
        this.setState({isLoggedIn: false});
    }

    render() {
        const isLoggedIn = this.state.isLoggedIn;
        let button;
        if (isLoggedIn) {
            // 엘리먼트 변수(버튼) = <'엘리먼트 반환 함수' onClick={이벤트 핸들러 함수} />
            button = <LogoutButton onClick={this.handleLogoutClick} />;
        } else {
            button = <LoginButton onClick={this.handleLoginClick} />;
        }

        return (
            <div>
                <Greetings isLoggedIn={isLoggedIn} />
                {button}	← 엘리먼트 변수 호출
            </div>
        );
    }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App isLoggedIn = {false} />);

위에 코드에서 각 컴포넌트는 아래 방식으로 작동한다.

  1. App 컴포넌트가 관리할 state인 isLoggedin을 constructor에서 false로 초기화한다.
  2. render() 함수에서 isLoggedin의 값을 읽어 분기문으로 버튼의 엘리먼트를 받아올 지역변수 button에 이벤트 핸들러 함수인 handleLoginClick() 을 호출한다.
  3. handleLoginClick()에서 isLoggedin의 값이 true로 변경되고 return문에서 외부함수인 Greeting 함수에 state값을 props로 넘긴다.
  4. Greetings 함수에서 props로 넘어온 값을 읽어 분기문으로 true일 때 버튼의 엘리먼트를 외부함수 GuestGreeting을 호출하여 render()함수에 엘리먼트를 반환한다.
  5. reder() 함수에 등록된 엘리먼트들을 페이지에 등록한다.

24. 조건문: 짧은 구문 - { condition && 실행문 }

JS에서는 논리연산자 &&를 사용하면 쉽게 엘리먼트를 조건부로 넣을 수 있다.

const Mailbox = ({unReadMessages = []}) => {
  return (
    <div>
      <h1>Hello!</h1>
      {unReadMessages.length > 0 &&
        <h2>
          {unReadMessages[0]}, {unReadMessages[1]}, {unReadMessages[2]} 
        </h2>
      }
      {unReadMessages.length === 0 &&
        <h2>
          Empty!
        </h2>
      }
    </div>
  )
}

const messages = ['Hello', 'Re: Hello', 'Re:Re: Hello'];

const root = ReactDOM.createRoot(document.getElementById('root')); 
// Hello!
// Hello, Re: Hello, Re:Re: Hello
root.render(<Mailbox unReadMessages={messages}/>);

25. 조건문: 컴포넌트 렌더링 막기(숨기기)

특정한 조건에 의해 컴포넌트 자체를 숨기고 싶을 때가 있다, 이때는 null 을 반환하면 된다.

아래의 예시는 공식 문서에 있는 예시 코드를 Hook을 이용하여, 클래스 컴포넌트에서 함수 컴포넌트로 변경한 예시 코드이다. ( 리스펙 송차장님 :) )

[솔루션 적용 전 - 클래스 컴포넌트]

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

[솔루션 적용 후 - 함수 컴포넌트]

const WarningBanner = ({warn}) => {
  if (!warn) {
    return null;
  } 

  return (
    <div>
      Warning!
    </div>
  );
}

const Page = () => {
  const [showWarning, SetWarning] = useState(false);

  const HandleToggleClick = () => {
    SetWarning(!(showWarning));
  }

  return (
    <div>
      <WarningBanner warn={showWarning}/>
      <button onClick={HandleToggleClick}>
        {showWarning ? 'Show': 'Hide'}
      </button>
    </div>
  );
}

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

26. 리스트와 Key: 엘리먼트 리스트

먼저 Javascript의 콜백 배열 메서드인 .map()을 리뷰해보자.

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
// [2, 4, 6, 8, 10]
console.log(doubled);

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

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

27. 리스트와 Key: 기본 리스트 컴포넌트

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

const NumberList = ({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'));

// ● 1
// ● 2
// ● 3
// ● 4
// ● 5
root.render(<NumberList numbers={numbers} />);

하지만, 위의 코드를 작성하여 실행하면, 정상적으로 페이지는 렌더링되지만 key를 넣어야 한다는 경고구문이 표시된다.

아래와 같이 key를 추가하여 key 누락 문제를 해결한다. 출력결과는 이전과 차이는 없다.

const NumberList = ({numbers}) => {
  const listItems = numbers.map((number) => 
    <li key={number.toString()}>	← key 추가
      {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} />);

28. 리스트와 Key: 왜 Key가 필요한가?

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다.

key는 엘리먼트에 안정적인 고유성을 부여하며, 대부분의 경우 데이터의 ID를 key로 사용한다. 만약 ID가 없다면 보통 인덱스값을 key로 사용하는데, 이렇게 되면 성능에 문제가 발생할 수 있다.

또한, 엘리먼트 리스트에 key를 부여하지 않으면, 인덱스값을 key로 사용하여 성능에 문제가 발생할 수 있다.

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

29. 리스트와 Key: Key로 컴포넌트 추출

map()으로 엘리먼트 리스트를 생성하는 컴포넌트와 props로 받아 렌더링하는 컴포넌트로 분리해야하거나 아래와 같이 사용하면 된다.

map() 함수 내부에서 key를 넣어 주는게 좋다.

const ListItem = ({numbers}) => {
  return (
    <li>{numbers}</li>
  );  
} 

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

  return(
      <ul>
        {listItems}
      </ul>
  );
} 

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));

// ●
// ●
// ●
// ●
// ●
root.render(<NumberList numbers={numbers} />);

30. 리스트와 Key: Key는 형제 사이에서만 고유한 값이어야 한다.

Key는 배열 안에서 형제 사이에서 고유해야 하고 전체 범위에서 고유할 필요는 없다. 두 개의 다른 배열을 만들 때 동일한 key를 사용할 수 있다.

const Blog = ({posts}) => {
  // map()에서 key를 지정
  const SideBar = (
    <ul>
      {posts.map((post) => 
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );

  // map()에서 key를 지정
  const Content = 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} />);

image-20220921163148037

React에서 key는 힌트를 제공하지만 컴포넌트로 전달하지는 않는다. 컴포넌트에서 key와 동일한 값이 필요하면 다른 이름의 prop으로 명시적으로 전달하도록하자. 이 때 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없다.

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

31. 리스트와 Key: 짧은 구문, JSX에 map() 포함시키기

렌더링할 영역에 중괄호로 직접 리스트 엘리먼트를 표시하여 인라인으로 처리가능하다. (단, 이 방식을 남발하면 클린코드가 되지 못해 좋지 못하니 주의하자.)

const ListItem = ({numbers}) => {
  return (
    <li>{numbers}</li>
  );  
} 

const NumberList = ({numbers}) => {
  // =========이전 방식==========
  // const listItems = numbers.map((number) => 
  //   <ListItem key={number.toString()} value={number} />
  // );

  // return(
  //     <ul>
  //       {listItems}
  //     </ul>
  // );

  
  // 렌더링할 영역에 중괄호로 직접 리스트 엘리먼트를 표시하여 인라인으로 처리가능
  return(
      <ul>
        {
          numbers.map((number) => 
          <ListItem key={number.toString()} value={number} />)
        }
      </ul>
  );
} 

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

32. 폼: 제어 컴포넌트

HTML 폼 엘리먼트는 폼 엘리먼트 자체가 내부 상태를 가지기 때문에, React의 다른 DOM 엘리먼트와 다르게 동작한다.

HTML 에서 type=”submit” 은 ‘폼을 전송하는 버튼의 역할’을 한다. 참고하도록 하자.

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

위 예시는 사용자가 폼을 제출하면 새로운 페이지(…/name=value)로 이동하는 기본 HTML 폼 동작을 수행한다.

React에서는 동일한 동작을 원한다면 그대로 사용해도 되지만, 대부분 JavaScript 함수로 폼의 제출을 처리하고 사용자가 폼에 입력한 데이터에 접근하도록 하는 것이 편리하다. 이를 위한 표준 방식은 “제어 컴포넌트”라 불리우는 기술을 이용한다.

제어 컴포넌트의 종류와 기능

‘제어 컴포넌트’란, React에 의해 값이 제어되는 입력 폼 엘리먼트를 말한다.

제어 컴포넌트는 아래와 같고 태그 속성으로 추가할 수 있다.

  • onChange: 폼의 입력 요소에 변경이 생기면 발생.
  • onInput: 요소의 값이 변경될 때 발생. (권장되지 않음.)
  • onSubmit: 폼 제출시 발생
// 폼 제출을 먼저 체크 → 입력 요소 변경 발생 체크
// 순서를 기억하자

return (
	<form onSubmit={handleSubmit}>
		<label>
			Name:
		<input type="text" value={value} onChange={handleChange} />
		</label>
		<input type="submit" value="Submit" />
	</form>
);

HTML에서 input, textarea, select 와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다.

그런데, React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되어 useState() 또는 setState()에 의해 업데이트할 수 있다.

const NameForm = () => {
  const [value, setValue] = useState('');

  const handleSubmit = (event) => {
    alert('A name was submitted: ' + value);
    event.preventDefault();
  }

  const handleChange = (event) => {
    setValue(event.target.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name:
        <input type="text" value={value} onChange={handleChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

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

제어 컴포넌트를 사용하면, input의 값을 항상 React state에 의해 결정된다.이렇게 하면 다른 UI 엘리먼트에 input의 값을 전달하거나 다른 이벤트 핸들러에서 값을 재설정할 수 있다.

33. 폼: textarea 태그

textarea 를 사용하는 폼은 value 속성을 사용하여, 한 줄 입력 폼과 비슷하게 작성하여 사용할 수 있다.

const EssayForm = () => {
  const [value, setValue] = useState('Please write an essay');

  const handleSubmit = (event) => {
    alert('An essay was submitted' + value);
    event.preventDefault();
  }

  const handleChange = (event) => {
    setValue(event.target.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Essay:
        <textarea value={value} onChange={handleChange} />
      </label>
      <input type='submit' value='Submit'/>
    </form>
  );
}

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

34. 폼: select 태그

HTML에서 select 태그로 드롭 다운 목록을 만들 수 있다.

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

React 에서는 selected 어트리뷰트를 사용하는 대신 최상단 select 태그에 value 속성을 사용한다.

한 곳에서 업데이트만 하면 되기 때문에 제어 컴포넌트에서 사용하기 편리하다.

const Flavor = () => {
  const [value, setValue] = useState('coconut');

  const handleSubmit = (event) => {
    alert('Your faovrite flavor is ' + value);
    event.preventDefault();
  } 

  const handleChange = (event) => {
    setValue(event.target.value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Pick your favorite flavor:
        <select value={value} onChange={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'></input>
    </form>
  );
}

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

35. 폼: 다중 입력 제어하기

여러 input 엘리먼트를 제어해야할 때, 각 엘리먼트에 name 을 추가하여, event.target.name 값을 통해 핸들러가 어떤 작업을 할지 선택하게 할 수 있다.

const Reservation = () => {
  const [inputs, setInputs] = useState({
    isGoing: false, 
    numberOfGuests: 2,
    name: null
  });

  const handleInputChange = (event) => {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
  
    setInputs(
      {name: value}
    );
  }

  return (
    <form>
      <label>
        Is going:
        <input 
          name='isGoing'
          type='checkbox'
          checked={inputs.isGoing}
          onChange={handleInputChange} />
      </label>
      <br />
      <label>
        Number of guests:
        <input
          name='numberOfGuests'
          type='number'
          value={inputs.numberOfGuests}
          onChange={handleInputChange} />
      </label>
    </form>
  );
}

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

위에 코드는 아래 방식으로 작동한다.

  1. Reservation 컴포넌트에서 관리할 state를 초기화한다. 각 state의 역할은 아래와 같다.
  • isGoing: 체크박스 요소의 초기값(checked)
  • numberOfGuests: 숫자 입력 요소의 초기값(6)
  • name: 변화가 생긴 입력 요소(체크박스 or 숫자 입력)의 값을 저장
  1. 입력 요소에 state의 입력 값(false, 2)을 각각 적용하여 렌더링한다.
  2. 만약, checkbox에 변화가 감지되면 이벤트핸들러(handleInputChange)에서 요소(event.target)를 type으로 구분하여, event.target.checked 값을 name에 저장하고 렌더링한다.
  3. 만약, 숫자 입력에 변화가 감지되면 이벤트 핸들러(handleInputChange)에서 요소를 type으로 구분하여, event.target.value 값을 name에 저장하고 렌더링한다.

36. 폼: 사용자가 입력할 수 없게 하기

입력 요소의 value를 undefined나 null로 설정하면, 사용자가 입력을 변경할 수 없게 할 수 있다.

// 이 요소는 수정할 수 없음
const Element = () => <input value="수정불가" />;

// 이 요소는 수정할 수 있음
const Element1 = () => <input value={null} />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <div>
    <Element />
    <hr />
    <Element1 />
  </div>
);

37. (연구 중…) State 끌어올리기

이제 어느정도 props와 state의 특성과 컴포넌트간 참조 방식을 이해하여 클래스 컴포넌트에서 함수 컴포넌트로 자유자재로 변형할 수 있게 되었다.

하지만… React 문서에서 제공하는 예제가 class로 작성되어있고, 이를 함수 컴포넌트로 전부 교체하여 작성 중… 막혔던 녀석은 ‘props’를 수정하는 방법이었다… props는 보통 컴포넌트를 호출할 때, 처음 초기화되고 바꿀 수 없는 것으로 알고 있는데…

예제가 있는 공식 문서에서는 아래와 같이 props를 변경할 수 있는 방법에 대해 설명하고 있다.

props는 읽기 전용입니다. temperature가 지역 state였을 때는 그 값을 변경하기 위해서 그저 TemperatureInputthis.setState()를 호출하는 걸로 충분했습니다. 그러나 이제 temperature가 부모로부터 prop로 전달되기 때문에 TemperatureInput은 그 값을 제어할 능력이 없습니다.

React에서는 보통 이 문제를 컴포넌트를 “제어” 가능하게 만드는 방식으로 해결합니다. DOM <input>valueonChange prop를 건네받는 것과 비슷한 방식으로, 사용자 정의된 TemperatureInput 역시 temperatureonTemperatureChange props를 자신의 부모인 Calculator로부터 건네받을 수 있습니다.

이렇게 표현을 하는데… 예제 코드대로 작성해보아도 해결이 되지 않는다…

예제에서 원하는 대로 두 곳의 입력란이 있다고 했을 때, 한 곳에서 입력한 값이 onChange()를 통해 감지되고, 다른 입력한 값으로 넘어가길 원한다면, 하나의 컴포넌트 안에 묶어 state로 입력값을 공유하게 하면 되지 않을까?

꼭 예제에서 제공하는 방식으로 해야할까… 등의 고민이 든다.

이 문제를 해결하는데 많은 시간이 걸릴 것 같아, 우선 다음 단원으로 넘어가고 props의 값을 수정하는 방법에 대해 고민을 더 해보아야겠다.

아래는 정상적으로 작동했던 코드와 문제가 발생했던 코드이다.

[문제 없이 작동했던 코드(변경 전)]

const BoilingVerdict = ({celsius}) => {
  if (celsius >= 100) {
    return <p>The water would boil.</p>
  } else {
    return <p>The water would not boil.</p>
  }
}

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit',
};

const toCelsius = (fahrenheit) => (fahrenheit - 32) * 5 / 9
const toFahrenheit = (celsius) => (celsius * 9 / 5) + 32;

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

const TemperatureInput = ({scale}) => {
  const [temperature, setState] = useState('');

  const handleChange = (e) => {
    setState(e.target.value);
  }

  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input
        value={temperature}
        onChange={handleChange} />
      <BoilingVerdict
        celsius={parseFloat(temperature)} />
    </fieldset>
  );
}

const Calculator = () => {
  return (
    <div>
      <TemperatureInput scale='c' />
      <TemperatureInput scale='f' />
    </div>
  );
}

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

[State 끌어올리기 중 문제가 발생한 코드]

const BoilingVerdict = ({celsius}) => {
  if (celsius >= 100) {
    return <p>The water would boil.</p>
  } else {
    return <p>The water would not boil.</p>
  }
}

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit',
};

const toCelsius = (fahrenheit) => (fahrenheit - 32) * 5 / 9
const toFahrenheit = (celsius) => (celsius * 9 / 5) + 32;

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

// temperature를 state → props 로 변경
const TemperatureInput = ({scale}, {temperature}, {onTemperatureChange}) => {
  // before: const [temperature, setState] = useState('');

  // temperature가 props로 변경되며, 값을 변경할 수 없음
  // 문제가 계속 발생함
  const handleChange = (e) => {
    onTemperatureChange = e.target.value;
  }


  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input
        value={temperature}
        onChange={handleChange} />
      <BoilingVerdict
        celsius={parseFloat(temperature)} />
    </fieldset>
  );
}

const Calculator = () => {
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c');

  const handleCelsiusChange = (temperature) => {
    setTemperature(temperature);
  }

  const handleFahrenheitChange = (temperature) => {
    setScale(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={handleCelsiusChange}
      />
      <TemperatureInput 
        scale='f' 
        temperature={fahrenheit}
        onTemperatureChange={handleFahrenheitChange}  
      />
      <BoilingVerdict
        celsius={parseFloat(celsius)} />
    </div>
  );
}

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

38. 합성과 상속

[문서 참조 링크]

39. React로 생각하기

[문서 참조 링크]

40. HOOK 예시

[예시 0]

const WarningBanner = ({warn}) => {
  if (warn) {
    alert('Warning!');
  } else {
    console.log('Not Warning!');
  }
}

const Page = () => {
  const [showWarning, SetWarning] = useState(false);

  const HandleToggleClick = () => {
    SetWarning(!(showWarning));
  }

  return (
    <div>
      <WarningBanner warn={showWarning}/>
      <button onClick={HandleToggleClick}>
        {showWarning ? 'Show': 'Hide'}
      </button>
    </div>
  );
}

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

[예시 1]

import {useState} from 'react';

const Example = () => {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

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

##

[예시 2]

import {useState} from 'react';

const App = () => {
  const [showWarning, setWarning] = useState(true);

  const HandleToggleClick = () => {
    setWarning(!(showWarning));
  }

  return (
    <div>
      <button onClick={HandleToggleClick}>
        {(showWarning).toString()}!
      </button>
    </div>
  );
}

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

Fin. [코드를 작성하며 실수했던 것들]

(1) “컴포넌트 호출이 안되는데?”

  • 컴포넌트의 이름의 첫글자가 대문자인지 확인하자.

(2) “버튼을 눌렀는데, 작동하지 않아”

  • 버튼 태그에 onClick={}과 이벤트 함수가 정의, 등록되었는지 확인하자.

(3) “state가 변경되지 않아”

  • setState는 함수이다. setState() 이런 식으로 작성되어있는지 확인하자.

(4) “render 함수에 element를 넣었는데도 컴파일 에러가 뜨네”

  • render() 는 함수이다. ()괄호가 붙어있는지 확인하자.
  • retrun 안에 div태그로 element가 감싸져 있는지 확인하자.
render() {
	return (
		<div>
			{element}
		</div>
	);
}

댓글남기기