본문 바로가기
  • GDG on campus Ewha Tech Blog
3-1기 스터디/Front-End 토이 프로젝트

[6주차] 컴포넌트 반복, 라이프사이클 메서드, 함수형 컴포넌트

by ssine 2021. 12. 22.

GDSC FE-Toy Project Study Plan
• 2차 프로젝트 진행 상황 공유
• React 책 기반 개념 공부 진행

Week 6) ~11/25
• 발표자
컴포넌트 반복, 라이프사이클 메서드: 장효신
함수형 컴포넌트: 하수민

 

6장 컴포넌트 반복

6.1 자바스크립트 배열의 map() 함수

arr.map(callback, [thisArg])

callback: 새로운 배열의 요소를 생성하는 함수, 파라미터는 다음 3가지가 있음

- currentValue: 현재 처리하고 있는 요소

- index: 현재 처리하고 있는 요소의 index값

- array: 현재 처리하고 있는 원본 배열

thisArg(선택): 콜백함수 내부에서 사용할 this 레퍼런스


데이터 배열을 컴포넌트 배열로 map하기

src/components/IterationSample.js

import React, { Component } from "react";

class IterationSample extends Component {
  render() {
    const names = ["눈사람", "얼음", "눈", "바람"];
    
    // map 함수에서JSX를 작성할때는 DOM 요소를 작성해도 되고, 컴포넌트를 사용해도 됨
    const nameList = names.map(name => <li>{name}</li>);

    return <ul>{nameList}</ul>;
  }
}
export default IterationSample;

 

실행하면 다음과 같이 배열이 렌더링된 것을 볼 수 있다. 

이렇게 출력에는 문제가 없지만 크롬 개발자 도구의 콘솔을 확인하면 Key가 없다는 경고 메시지를 볼 수 있다.


Key

key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있는지 알아낼 때 사용함

예를 들어 유동적인 데이터를 다룰 때는 원소를 새로 생성할 수도, 제거할 수도, 수정할 수도 있다. 


key가 필요한 이유

key가 없을 때는 가상 DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지하지만

key가 있다면 이 값을 사용해서 어떤 원소에 변화가 생겼는지 빠르게 알아낼 수 있다. 


key 설정

key를 설정할 때는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설정하면 된다. 

Key값은 언제나 유일해야 하기 때문에 데이터가 가진 고윳값을 key 값으로 설정해야 한다. 

예를 들어 다음과 같이 게시판의 게시물을 렌더링한다면 게시물 번호를 key 값으로 설정해야 한다.

const articleList = articles.map(article=> (
	<Article
    	title={article.title}
        writer={article.writer}
        key={article.id}
     />
);

 

컴포넌트에 고유 번호가 없을 때는 map 함수에 전달되는 콜백 함수의 인수인 index를 사용하면 된다.

앞에서 만들었던 예제 컴포넌트를 다음과 같이 수정해보자

 

    const names = ["눈사람", "얼음", "눈", "바람"];
    const nameList = names.map((name, index) => <li key={index}>{name}</li>);

 

이제 더이상 개발자 도구에서 경고 메시지를 출력하지 않는다.


삭제 함수를 간단하게하고 싶다면 filter를 사용할 수도 있다. 

filter는 배열에서 특정 조건을 만족하는 값들만 추출해서 새로운 배열을 만든다.

handleRemove = (index) => {
	const {names} = this.state;
    this.setState({
    	names: nams.filter((item, i) => i!== index)
    });
}

 

정리

  • map을 이용해서 반복되는 데이터를 렌더링할 수 있음
  • 컴포넌트 배열을 렌더링할 때는 key값은 항상 유일해야 한다.
  • key값 중복된다면 렌더링 과정에서 오류 발생
  • 상태 안에서 배열을 변경할 때는 배열에 직접 접근 x
💡 concat, slice, spread 연산자, filter 함수 등을 이용해서 새로운 배열을 만든 후, setState 메서드를 적용한다는 것을 기억하자 (리덕스에도 동일하게 작용)

7장 컴포넌트의 라이프사이클 메서드

: 컴포넌트의 수명주기를 관리하는 메서드

모든 리액트 컴포넌트는 라이프사이클(수명 주기)이 존재한다. 

필요한 이유

: 컴포넌트가 처음 렌더링될 때 해야할 작업이 있고

컴포넌트가 업데이트되기 전에 해야할 작업이 있다.

이때 컴포넌트의 라이프사이클 메서드를 사용한다.


라이프사이클 메서드의 종류

라이프사이클 메서드의 종류는 총 열 가지이다. 

Will 접두사가 붙은 메서드 => 어떤 작업을 작동하기 에 실행되는 메서드

Did 접두사가 붙은 메서드 => 어떤 작업이 작동한 에 실행되는 메서드

라이프사이클은 총 세가지로 마운트, 업데이트, 언마운트로 나뉜다. 

 


마운트

DOM이 생성되고 웹브라우저에서 나타나는 것

이때 호출하는 메서드

 constructor: 컴포넌트를 새로 만들 때마다 호출되는 클래스 생성자 메서드

 getDerivedStateFromProps: props에 있는 값을 state에 넣을 때 사용하는 메서드

 render: UI를 렌더링하는 메서드

 componentDidMount: 컴포넌트가 웹 브라우저상에 나타난 후 호출하는 메서드

 


업데이트

컴포넌트는 다음 네 가지 경우에 업데이트함

1. props가 바뀔 때

2. state가 바뀔 때

3. 부모 컴포넌트가 리렌더링될 때

4. this.forceUpdate로 강제로 렌더링을 트리거할 때

 

이렇게 컴포넌트를 업데이트할 때는 다음 메서드를 호출함

 

 getDerivedStateFromProps: 이 메서드는 마운트 과정에서도 호출되며, 업데이트가 시작하기 전에도 호출됨. props의 변화에 따라 state 값에도 변화를 주고 싶을 때 사용합니다.

 shouldComponentUpdate: 컴포넌트가 리렌더링을 해야 할지 말아야 할지를 결정하는 메서드. 이 메서드에서는 true나 false 값을 반환해야 함 true를 반환하면 다음 라이프사이클 메서드를 계속 실행하고, false를 반환하면 작업을 중지함. 즉, 컴포넌트가 리렌더링되지 않는다.

 render: 컴포넌트를 리렌더링함

 getSnapshotBeforeUpdate: 컴포넌트 변화를 DOM에 반영하기 바로 직전에 호출하는 메서드

 componentDidUpdate: 컴포넌트의 업데이트 작업이 끝난 후 호출하는 메서드


 

라이프사이클 메서드 하나하나씩 살펴보기

 

render() 함수

render(){ ... }

- 컴포넌트 모양새를 정의하는 가장 중요한 메서드

- 라이프사이클 메서드 중 유일한 필수 메서드

- render() 안에서 this.props와 this.state에 접근할 수 있으며, 리액트 요소 반환 (div 태그나 컴포넌트)

- 아무것도 보여주고 싶지 않다면 null이나 false를 반환하도록 한다.

- 이 메소드 안에서는 절대로 state를 변형하면 안되고 웹 브라우저에 접근해서도 안된다. => setState...?

- DOM 정보를 가져오거나 변화를 줄 때는 componentDidMount에서 처리해야 한다. 


consturctor 메서드

constructor(props){ ... }

- 컴포넌트의 생성자 메서드로 컴포넌트를 만들 때 처음으로 실행됨

- 초기 state를 정할 수 있음

사용 목적

  • this.state에 객체를 할당하여 state 초기화
  • 인스턴스에 이벤트 처리 메서드를 바인딩

constructor => this.state를 유일하게 사용할 수 있고 (setState()를 호출하면 안 됨)

나머지는 this.setState로 state값을 변경

constructor(props) {
  super(props);
  // 다음과 같이 constructor 안에서 초기 state값을 지정할 수 있음
  // 이때 주의할 점은 여기서 this.setState()를 호출하면 안 됨
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

getDerivedStateFromProps 메서드

리엑트 v16.3 이후에 새로 만든 라이프사이클 메서드로 props로 받아온 값을 state에 동기화시키는 용도로 사용

컴포넌트를 마운트하거나 props를 변경할 때 호출함

이 메서드는 시간이 흐름에 따라 변하는 props에 state가 의존하는 아주 드문 사용례을 위해서 존재함 

static getDerivedStateFromProps(nextProps, prevState){
	if(nextProps.value !== prevState.value){
    // props로 받아온 값과 state 값을 비교해서 다르면 state값에 동기화할 수 있음
    return { value: nextProps.value };
    }
    return null; // state를 변경할 필요가 없다면 null을 반환
}

componentDidMount 메서드

컴포넌트를 만들고, 첫 렌더링을 다 마친 에 실행함

이 안에서 자바스크립트 라이브러리, 프레임워크의 함수를 호출하거나 이벤트 등록함. 외부에서 데이터를 불러와야 한다면, 네트워크 요청하는 위치!


shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState){ ... }

props나 state를 변경했을 때, 리렌더링을 시작할지 여부를 지정하는 메서드다. => 반드시 true나 false 리턴

컴포넌트를 만들 때 이 메서드를 따로 생성하지 않으면 기본적으로 언제나 true 값을 반환 => 디폴트 값은 true

프로젝트 성능을 최적화할 때, 리렌더링을 방지할 때는 false 값을 반환


componentDidUpdate 메서드

componentDidUpdate(prevProps, prevState, snapshot) { ... }

리렌더링을 완료한 후 실행함. 업데이트가 끝난 직후이므로, DOM 관련 처리를 해도 무방함

prevProps 또는 prevState를 사용하여 컴포넌트가 이전에 가졌던 데이터에 접근할 수 있음


8장 함수형 컴포넌트

  • 가장 기본적인 Hook
  • 함수 컴포넌트에서도 가변적인 상태를 지닐 수 있게 해줌 ➡ 함수 컴포넌트에서 상태를 관리해야 할 때 이 Hook을 사용

 

<Counter.js>   useState 기능을 사용해 숫자 카운터 구현

import { useState } from 'react'; // useState는 코드 상단에서 import 구문을  통해 불러옴

const Counter = () => {
	const [value, setValue] = useState(0); // useState함수의 파라미터에는 상태의 기본값 넣어 줌
    //함수가 호출되면 배열 반환. 첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 함수
    //이 함수에 파라미터를 넣어 호출하면 전달받은 파라미터로 값이 바뀌고 컴포넌트가 정상적으로 리렌더링됨
    
    return (
    	<div>
        	<p>
        		현재 카운터 값은 <b>{value}</b>입니다.
			</p>
        	<button onClick={() => setValue(value + 1)}>+1</button>
        	<button onClick={() => setValue(value - 1)}>-1</button>
		</div>
	);
};

export default Counter;

 

<App.js>

import Counter from './Counter';

const App = () => { // Counter 컴포넌트 렌더링
	return <Counter />;
};

export default App;

 

터미널에 yarn start 명령어를 입력해 개발 서버를 구동


useState로 카운터 구현

함수 컴포넌트에서 상태 관리를 하기 위해 컴포넌트 코드를 굳이 클래스 형태로 변환할 필요가 없어 매우 편리

1-1) useState를 여러 번 사용하기

  • 하나의 usetState 함수는 하나의 상태 값만 관리 가능
  • 컴포넌트에서 관리해야 할 상태가 여러 개라면 useState을 여러 번 사용

2. useEffect
  • 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook
  • 클래스형 컴포넌트의 componentDidMount와 componentDidUpdate 합친 형태
  •  

<Info.js> 기존에 만들었던 위의 Info 컴포넌트에 useEffect 추가

import { useState, useEffect } from 'react'; 

const Info = () => {
	const [name, setName] = useState('');
    const [nickname, setNickname] = useState('');
    useEffect(() => {
    	console.log('렌더링 완료!');
        console.log({
        	name,
            nickname
		});
	});
    
... // 위의 Info.js와 동일

브라우저에서 개발자 도구를 열고 인풋 내용 변경하기


useEffect

2.1) 마운트될 때만 실행하고 싶을 때

useEffect에서 설정한 함수를 컴포넌트가 화면에 맨 처음 렌더링될 때만 실행, 업데이트될 때는 실행하지 않으려면 함수의 두 번째 파라미터로 비어 있는 배열

<Info.js>에서 useEffect 코드 변경

useEffect(() => {
	console.log('마운트될 때만 실행');
}, []);

다시 브라우저를 열어서 인풋을 수정해보면 컴포넌트가 처음 나타날 때만 콘솔에 문구가 나타나고, 그 이후에는 나타나지 않음


마운트될 때만 실행

2.2) 특정 값이 업데이트될 때만 실행하고 싶을 때

useEffect를 사용할 때, 특정 값이 변경될 때만 호출하고 싶은 경우 <클래스형 컴포넌트>

componentDidUpdate(prevProps, prevState) {
	if(prevProps.value !== this.props.value) {
    	doSomething();
	}
}

이 코드는 props 안에 들어 있는 value 값이 바뀔 때만 특정 작업 수행

이러한 작업을 useEffect에서 해야 한다면 useEffect의 두 번째 파라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어 주면 됨

<Info.js>에서 useEffect 수정

useEffect(() => {
	console.log(name);
}, [name]);

배열 안에는 useState를 통해 관리하고 있는 상태를 넣어 주어도 되고, props로 전달받은 값을 넣어 주어도 됨


특정 값이 업데이트될 때만 실행

3.3) 뒷정리하기

  • useEffect는 기본적으로 렌더링되고 난 직후마다 실행되며, 두 번째 파라미터 배열에 무엇을 넣는지에 따라 실행되는 조건 달라짐
  • 컴포넌트가 언마운트되기 전이나 업데이트되기 직전에 어떠한 작업을 수행하고 싶다면 useEffect에서 뒷정리(cleanup)함수를 반환해야 함

<Info.js>에서 useEffect 수정

useEffect(() => {
	console.log('effect');
    console.log(name);
    return () => {
    	console.log('cleanup');
        console.log(name);
	};
}, [name]);

 

<App.js> Info 컴포넌트의 가시성을 바꿀 수 있게 수정

import { useState } from 'react';
import Info from './Info';

const App = () => {
	const [visible, setVisible] = useState(false);
    return (
    	<div>
        	<button
            	onClick={() => {
                	setVisible(!visible);
				}}
			>
            	{visible ? '숨기기' : '보이기'}
			</button>
            <hr />
            {visible && <Info />}
		</div>
	);
};

export default App;

useEffect 뒷정리

3. useReducer
  • useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트를 해 주고 싶을 때 사용하는 Hook
  • 리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션(action) 값을 전달받아 새로운 상태를 반환하는 함수
  • 리듀서 함수에서 새로운 상태를 만날 때는 반드시 불변성을 지켜 줘야 함
리듀서(reducer)라는 개념은 후에 17장. 리덕스를 배울 때 자세하게 나옴

function reducer(state, action){
	return { ... }; // 불변성을 지키면서 업데이트한 새로운 상태를 반환
}

// 액션 값은 주로 아래와 같은 형태
{
	type: 'INCREMENT',
    // 다른 값들이 필요하면 추가로 들어감
}
17장. 리덕스에서 사용하는 액션 객체에는 어떤 액션인지 알려 주는 type 필드가 꼭 있어야 하지만, useReducer에서 사용하는 액션 객체는 반드시 type을 지니고 있을 필요 없음. 객체가 아니라 문자열이나 숫자여도 괜찮음.

 

3.1) 카운터 구현

<Counter.js> useReducer를 사용해 기존의 Counter 컴포넌트 다시 구현

import { useReducer } from 'react';

function reducer(state, action) {
	//action.type에 따라 다른 작업 수행
    switch (action.type) {
    	case 'INCREMENT':
        	return { value: state.value + 1 };
		case 'DECREMENT':
        	return { value: state.value - 1 };
		default:
            return state; //아무것도 해당되지 않을 때 기존 상태 반환
	}
}
	
const Counter = () => {
	const [state, dispatch] = useReducer(reducer, { value: 0 });
    // useReducer의 첫 번째 파라미터에는 리듀서 함수를 넣고, 두 번째 파라미터에는 해당 리듀서의 기본값
    // 이 Hook을 사용하면 state 값과 dispatch 함수 받아 옴
    // state은 현재 가리키고 있는 상태, dispatch는 액션을 발생시키는 함수
    // dispatch(action) 형태로, 함수 안에 파라미터로 액션 값을 넣어 주면 리듀서 함수가 호출되는 구조
    
    return (
    	<div>
        	<p>
        		현재 카운터 값은 <b>{state.value}</b>입니다.
			</p>
        	<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
        	<button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
		</div>
	);
};

export default Counter;

useReducer 가장 큰 장점 : 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 뺄 수 있음

 

3.2) 인풋 상태 관리

  • useReducer를 사용해 Info 컴포넌트에서 인풋 상태 관리
  • 기존에는 인풋이 여러 개여서 useState 여러 번 사용함
  • useReducer를 사용하면 기존에 클래스형 컴포넌트에서 input 태그에 name 값을 할당하고 e.target.name을 참조하여 setState를 해 준 것과 유사한 방식으로 작업 처리 가능

 

4. useMemo

useMemo를 사용하면 함수 컴포넌트 내부에서 발생하는 연산을 최적화 가능

<Average.js> 리스트에 숫자를 추가하면 추가된 숫자들의 평균을 보여주는 함수 컴포넌트

App.js에서 이 컴포넌트 렌더링하면

  • 그런데 숫자를 등록할 때뿐만 아니라 인풋 내용이 수정될 때도 우리가 만든 getAverage 함수가 호출됨.
  • 인풋 내용이 바뀔 때는 평균값을 다시 계산할 필요가 없음 ➡useMemo Hook을 사용하면 이러한 작업 최적화 가능
  • 렌더링하는 과정에서 특정 값이 바뀌었을 때만 연산 실행하고, 원하는 값이 바뀌지 않았다면 이전에 연산했던 결과를 다시 사용


5. useCallback
  • useCallback은 useMemo와 상당히 비슷한 함수
  • 주로 렌더링 성능을 최적화해야 하는 상황에서 사용
  • 이 Hook을 사용하면 만들어 놨던 함수 재사용 가능

 

  • 위에 구현한 Average 컴포넌트에서 onChange와 onInsert 함수를 선언해줬는데, 이렇게 선언하면 컴포넌트가 리렌더링될 때마다 새로 만들어진 함수를 사용
  • 대부분의 경우 이러한 방식은 문제가 없지만, 컴포넌트의 렌더링이 자주 발생하거나 렌더링해야 할 컴포넌트의 개수가 많아지면 이 부분을 최적화해 주는 것이 좋음

<Average.js>

import { useState, useMemo, useCallback } from 'react'; 

...

const Average = () => {
	const [list, setList] = useState([]);
    const [number, setNumber] = useState('');
    
    const onChange = useCallback(e => {
    	setNumber(e.target.value);
	}, []); // 컴포넌트가 처음 렌더링될 때만 함수 생성
    const onInsert = useCallback(() => {
    	const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
	}, [number, list]); // number 혹은 list가 바뀌었을 때만 함수 생성
    
    ...
  • useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수, 두 번째 파라미터에는 배열을 넣음 ➡ 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시해야 함
  • onChange처럼 비어 있는 배열을 넣게 되면 컴포넌트가 렌더링될 때 만들었던 함수를 계속해서 재사용하게 되며 onInsert처럼 배열 안에 number와 list를 넣게 되면 인풋 내용이 바뀌거나 새로운 항목이 추가될 때 새로 만들어진 함수를 사용하게 됨
  • 함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 파라미터 안에 포함시켜 줘야 함
  • 예를 들어 onChange의 경우 기존값을 조회하지 않고 바로 설정만 하기 때문에 배열이 비어 있어도 괜찮지만, onInsert는 기존의 number와 list를 조회해서 nextList를 생성하기 때문에 배열 안에 number와 list를 꼭 넣어 줘야 함
6. useRef

useRef Hook은 함수 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해줌

<Average.js> 등록 버튼을 눌렀을 때 포커스가 인풋 쪽으로 넘어가도록

import { useState, useMemo, useCallback, useRef } from 'react'; 

...

const Average = () => {
	const [list, setList] = useState([]);
    const [number, setNumber] = useState('');
    const inputEl = useRef(null); //
    
    const onChange = useCallback(e => {
    	setNumber(e.target.value);
	}, []); 
    const onInsert = useCallback(() => {
    	const nextList = list.concat(parseInt(number));
        setList(nextList);
        setNumber('');
        inputEl.current.focus(); //
	}, [number, list]); 
    
    const avg = useMemo(() => getAverage(list), [list]); //

    return (
        <div>
            <input value={number} onChange={onChange} ref=inputEl /> //
            ...

useRef를 사용하여 ref를 설정하면 useRef를 통해 만든 객체 안의 current 값이 실제 엘리먼트를 가리킴

 

6.1) 로컬 변수 사용

컴포넌트 로컬 변수를 사용해야 할 때도 useRef 사용 가능

로컬 변수 : 렌더링과 상관없이 바뀔 수 있는 값

클래스 형태로 작성된 컴포넌트의 경우에 로컬 변수를 사용해야 할 때 코드

import { Component } from 'react';

class MyComponent extends Component {
	id = 1
    setId = (n) => {
    	this.id = n;
	}
    printId = () => {
    	console.log(this.id);
	}
    render() {
    	return (
        	<div>
            	MyComponent
			</div>
		);
	}
}

export default MyComponent;

 

함수 컴포넌트로 작성하면

import { useRef } from 'react';

const RefSample = () => {
	const id = useRef(1);
    const setId = (n) => {
    	id.current = n;
	}
    const printId = () => {
    	console.log(id.current);
	}
    return (
    	<div>
        	refsample
		</div>
	);
};

export default RefSample;

📍 ref 안의 값이 바뀌어도 컴포넌트가 렌더링되지 않는다는 점 주의 ➡ 렌더링과 관련되지 않은 값을 관리할 때만 이러한 방식 사용하기


9. 정리
  • 리액트에서 Hooks 패턴을 사용하면 클래스형 컴포넌트를 작성하지 않고도 대부분의 기능을 구현 가능
  • useState 혹은 useReducer를 통해 구현할 수 있더라도 기존의 setState을 사용하는 방식이 잘못된 것은 아님
  • 리액트 매뉴얼에 따르면, 기존의 클래스형 컴포넌트는 앞으로도 계속해서 지원될 예정 ➡ 유지 보수하고 있는 프로젝트에서 클래스형 컴포넌트를 사용하고 있다면, 굳이 함수 컴포넌트와 Hooks를 사용하는 형태로 전환할 필요는 없음
  • 다만, 매뉴얼에서는 새로 작성하는 컴포넌트의 경우 함수 컴포넌트와 Hooks 사용할 것을 권장
  • 앞으로 프로젝트를 개발할 때는 함수 컴포넌트의 사용을 첫 번째 옵션으로 두고, 꼭 필요한 상황에서만 클래스형 컴포넌트 구현하기
 

댓글