[2024] 한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
isdiscodead의 프로필 사진 isdiscodead
들어가며
수강생 커뮤니티 참가 안내

sadasdasdfasfasd

JavaScript 기본
1.4) 변수와 상수

React -> 자바스크립트 UI 라이브러리 (오픈소스)

React의 파생 기술인 ReactNative로 크로스플랫폼 어플리케이션 개발 가능

Node.js 기초
3.1) Node.js를 소개합니다

Node.js를 하는 이유

자바스크립트 -> 브라우저 내장 자바스크립트 엔진을 이용해 실행됨 ( 브라우저마다 엔진이 다름, 크롬의 v8 등 )

js를 브라우저에서만 사용하지 않고, 밖에서 사용할 수 있도록 v8 엔진( c++로 쓰임 )을 빼냄 -> node.js

즉 Node.js는 JavaScript의 실행 환경임 + React의 근간

-> javascript로 web server나 pc 프로그램도 만들 수 있게됨

web server란 요청( request )를 받으면 웹으로 응답하는 서버

  • 요청을 누가 하느냐에 따라 클라이언트가 달라짐

3.2) Node.js 설치하기

node.js LTS 버전 설치 -> node -v 로 버전 확인

npm은 node 설치 시 자동으로 설치됨 ( 패키지 매니저 )

npm -v 로 버전 확인 가능

프로젝트 진행 시 폴더 내에서 기능 별로 파일 사용

3.3) Node.js 사용하기

GUI( Graphic User Interface ) -> 마우스 등으로 컴퓨터( 운영체제 )에게 명령

CLI( Command Line Interface ) -> 터미널에서 명령어로 직접 운영체제에 명령

모듈 = 어떤 기능을 담당하는 각각의 파일 ...

node.js 모듈을 다른 파일에서 사용하도록 내보내기는 exports 키워드로 진행할 수 있음

// node.js는 객체 단위로 모듈 내보내기 진행됨
module.exports = {
	moduleName: "calc module",
	add: add,
	sub: sub,
};

이후 다른 파일에서 node 내장 함수인 require 함수를 사용해서 불러올 수 있음

const calc = require("./calc");

console.log(calc);
console.log(calc.add(1,2));
console.log(calc.add(4,5));
console.log(calc.sub(10,2));

Common JS

require, exports 같은 기능 등을 포함하는 모듈 시스템

비슷하게... ES Module도 존재

3.4) Node.js 모듈 시스템 이해하기

NPM

Node Package Manager -> Node.js의 패키지 관리 도구

Package란 다른 사람들이 만들어둔 Node 모듈 ...

패키지 만들기

root 폴더 = 패키지 파일들을 담아두는 폴더

패키지는 초기 설정이 필요함 !

  1. root 폴더에서 npm init 명령어 실행

  2. 패키지 명, author 등 정보 입력 -> package.json 생성됨

{
  "name": "pacakge-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  	"start": "node index.js"
  },
  "author": "isdiscodead",
  "license": "ISC"
}

-> main의 경우 처음 실행될 js 파일을 뜻하고 scripts는 자주 실행되는 명령어 등을 정의해두는 것

-> npm start 명령어로 이 패키지 실행 가능

외부 node package 사용하기

https://www.npmjs.com/ 에서 다양한 패키지 확인 가능

randomcolor 패키지 -> 랜덤 컬러 코드 줌

npm i randomcolor

 -> package.jsondependancies 에서 설치된 외부 패키지의 정보를 확인할 수 있음. 버전 앞의 ^는 ~버전 이상을 뜻함

-> node_modules 폴더에는 실제 패키지 파일이 보관됨

-> package-lock.json에서는 정확한 외부 패키지 버전이 기록됨

->

React.js 개론
4.1) React.js를 소개합니다

리액트를 사용하는 이유 1 : Component

 중복 코드를 전부 수정해야 되는 경우 -> shotgun surgery ... 유지 보수 문제 생김

 보통 웹 페이지는 공통 요소를 많이 가지고 있음 -> 별도의 컴포넌트로 만들어서 사용하기 !

React는 컴포넌트 기반의 UI 라이브러리이므로 코드 재사용성이 높음

리액트를 사용하는 이유 2 : 선언형 프로그래밍

명령형 프로그래밍 -> 모든 절차를 하나하나 다 나열해야 함

ex) jQuery

선언형 프로그래밍 -> 목적만 바로 명시

ex) React

리액트를 사용하는 이유 3 : Vritual DOM

DOM ( Document Object Model ) = 브라우저에서 웹 페이지를 보여주기 위해서 사용하는 Tree 형태의 가상 객체

웹 페이지에 요소가 추가/삭제 될 경우 잦은 업데이트로 성능 저하 생김 -> React는 Virtual DOM을 활용하므로 렌더링 없이 업데이트 필요한 경우에 업데이트함

4.2) 첫 React App 생성하기

React App을 만드는 방법

Webpack -> 다수의 자바스크립트 파일을 하나의 파일로 합쳐주는 모듈 번들 라이브러리

Babel -> JSX 등의 쉽고 직관적인 자바스크립트 문법을 사용할 수 있도록 해주는 라이브러리

create react app으로 리액트 boiler plate 패키지 사용 -> node.js 패키지의 구성 요소와 동일함

npx -> 설치하기 싫은 패키지를 1회성으로 사용할 수 있는 명령어... npx -v 로 버전 확인 가능

npx create-react-app reactExam1

실패할 경우 node.js 버전 문제일 확률이 높음 ~!!

npm start 명령어로 리액트 앱 실행 가능

4.3) React App 구동원리 살펴보기

JSX

Javascript + xml로 컴포넌트 생성 시 유용하게 사용됨

jsx의 표현식은 반드시 하나의 부모를 가져야 하므로 최상위 태그로 가장 바깥을 감싸줘야 한다 !!

만약에 묶고 싶지 않다면 React.fragment 사용

<React.Fragment>
   <MyHeader />
   <header className="App-header">
     <h2>안녕 리액트 {name}</h2>
   </header>
</React.Fragment>

jsx는 html 태그 안에 {}로 javascript 값을 사용할 수 있음 ( 숫자나 문자열 수식만 가능 )

React.js 입문
5.4) Props로 데이터 전달하기

Props

컴포넌트에 데이터를 전달하기 위한 기본적이고 효율적인 방법

상위 컴포넌트에서 값을 보내주면 매개변수를 통해 받아올 수 있음

  • defaultProps로 전달 받지 못한 props의 기본 값 설정 가능 !!

  • 정적인 데이터 뿐만 아니라 동적인 데이터도 전달 가능, 즉 부모에게서 받아오는 props 변경 시에도 리렌더링이 일어남

  • 컴포넌트 자체도 props로 전달 가능함 !!

5.6) State로 상태관리하기

상태 State

리액트의 가장 중요한 개념 -> 컴포넌트가 가지고 관리하는 동적인 값으로 값과 상태 변화 함수를 가짐

useState 메서드를 import해서 상태 관리 가능

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

컴포넌트는 자신이 가진 State가 변화하면, 화면을 다시 그려 rerender함 ( 화면을 새로 그림 )

컴포넌트 하나에 여러 개의 스테이트가 존재해도 ok

 

5.10) useRef로 컴포넌트의 변수 생성하기

useRef()의 반환형인 React.MutableRefObject<>는 DOM에 접근할 수 있게 함 ~~

const contentInput = useRef();

            <textarea 
                ref={contentInput}
                name="content"
                value={state.content}
                onChange={handleChangeState}
            />

contentInput.current.focus();
라이프사이클
7.1) 라이프사이클이란?

Life Cycle = 생애 주기 ( 탄생 ~ 죽음 )

생애 주기의 단계 별로 행동 양상, 특징 등이 달라짐 -> 개발에서도 마찬가지

프로그램의 실행 ~ return ... React도 이런 생명 주기를 가짐 !

Mounting( 탄생 ) : 화면에 나타나는것

Updating( 변화 ) : 업데이트, 즉 리렌더

Unmounting( 죽음 ) : 화면에서 사라짐

컴포넌트의 생명 주기를 제어한다는 것 = 각 생애 주기에서의 초기화 작업, 예외 처리 작업, 메모리 정리 작업 등을 처리하는 것

리액트는 기본적으로 각 라이프 사이클에서 사용되는 메서드( ComponentDidMount, ComponentDidUpdate, ComponentWillUnmount )가 존재하지만 Class형 리액트에서만 사용 가능

따라서 함수형 컴포넌트에서는 useEffect라는 React hooks를 사용함

hooks를 사용해서라도 함수형을 쓰는 이유 -> class형 컴포넌트의 길어지고 중복되는 코드, 가독성 문제 등 ...

useEffect(() => {
    // callback 함수 
}, []); // 이 의존성 배열 내에 들어있는 값이 변화하면 콜백 함수가 실행됨 

-> 의존성 배열이 빈 배열이면 component가 mount 되는 시점에만 콜백 함수가 실행됨

// mount 시점에 실행됨 
	useEffect(() => {
		console.log("Mount");
		
		return () => {
			// Unmount 시점에 실행
			console.log("Unmount");
		};
	}, []);

// update
	useEffect(() => {
		console.log("Update");
	});
	
	// 특정 값 update 
	useEffect(() => {
		console.log(`count is updated : ${count}`);
	}, [count]);
7.4) React 개발자 도구 사용하기

React Developer Tools 크롬 확장 프로그램

세부 정보 페이지에서 사이트 액세스 - 모든 사이트에서

파일 url에 대한 액세스 허용 - 허용

설정 후 크롬 재시작, npm start로 프로젝트 열면 오른쪽에 리액트 도구 뜸

개발자 도구에서 >> 모양 눌러보면 새로운 기능인 Components가 생김

  • props 등을 포함한 컴포넌트 계층 구조를 볼 수 있음 !!

  • View Setting에서 설정해두면 rerender 되는 컴포넌트가 하이라이팅됨

프로젝트2. 투두리스트
8.2) UI 구현하기

리액트에서 입력 값 처리하기

state 사용해서 관리!

  1. useState로 state 생성

  2. input 요소의 onChange 이벤트에서 setState 호출

    1. onChange 이벤트의 이벤트 객체 e는 target.value 존재

    2. 여러 input 요소의 state 관리 방식이 비슷하면 하나의 state라는 객체로 묶어서 관리 가능

      <textarea 
            value={state.content}
            onChange={(e) => {
                setState({
                    ...state,
                    content: e.target.value,
                });
             }}
      />
8.3) 기능 구현 준비하기

JSON Placeholder로 api 대용해서 사용 ...

post, todo, users, comments 등 몇가지 데이터 샘플 존재함

const getData = async() => { // promise를 반환하는 비동기 함수 
	const res = await fetch('https://jsonplaceholder.typicode.com/comments/https://jsonplaceholder.typicode.com/comments/')
	.then((res) => res.json() );
	
	// body는 내용, email를 작성자로 ...
	const initData = res.slice(0, 20).map((it) => {  // 0 ~ 19 인덱스
		return {
			author: it.email,
			content: it.body,
			emotion: Math.floor(Math.random() * 5) + 1, // ( 0 ~ 4 ) + 1
			created_date: new Date().getTime(),
			id: dataId.current ++
		}
	});
	
	setData(initData);
};


// mount 시에 데이터 불러오기 
useEffect(() => {
	getData();
}, []);
8.4) Create - 투두 추가하기

리액트는 단방향( 부모 -> 자식 )으로만 props 전달 가능

따라서 데이터는 부모에서 state로 처리하고 setData와 data로 CRUD ...

이벤트는 setData로 아래 -> 위만 흐르고

데이터는 data를 통해 위 -> 아래로만 흐름

 

데이터 수정 시에는 항상 객체 spread(...) 후 수정할 내용 추가하기 !!

8.5) Read - 투두리스트 렌더링하기

map으로 컴포넌트를 만들어줄 땐 항상 key를 가져야 함! 자체 데이터에 id가 없을 경우 map 함수의 두 번째 인수인 idx 사용 map((data, idx) => ...

import DiaryItem from "./DiaryItem";

const DiaryList = ({ diaryList }) => {
    // console.log(diaryList);
    return (
        <div className="DiaryList">
            <h2>일기 리스트</h2>
            <h4>{diaryList.length}개의 일기가 있습니다.</h4>
            <div>
                { diaryList.map((it) => (
                    <DiaryItem key={it.id} {...it} />
                ))}
            </div>
        </div>
    );
};

DiaryList.defaultProps = {
    diaryList: [], // 기본값으로 빈 배열을 넣어 undefined 오류 방지
}

export default DiaryList;
8.7) Delete - 투두 삭제하기

삭제 기능

App.js에서 onDelete 만들고 List -> Item으로 props에 전달 ( props drilling )

  const onDelete = ( targetId ) => {
	  // console.log(`${targetId}가 삭제되었습니다.`);
	  const newDiaryList = data.filter((it) => it.id !== targetId);
	  setData(newDiaryList);
  };
useReducer
9.1) useReducer를 소개합니다

App 컴포넌트 -> 상태 변화 함수가 create, edit, remove로 존재하여 복잡함...

App 컴포넌트 외에는 다른 곳에 있을 수 X ( App 컴포넌트의 data를 참조해야하므로 )

-> 컴포넌트에서 상태변화 로직을 분리하자 !

useReducer

useState를 대체할 수 있는 중요한 기능 !

const [state, dispatch] = useReducer(reducer, defaultValue);

 

dispatch -> 상태 변화를 raise

dispatch는 action 객체를 전달 받음 ( 무슨 action인지 확인하기 위함 )

onClick={ () => dispatch( { type: 1000 }) }

 

reducer -> 상태 변화를 처리하여 state를 return

현재 state와 action을 받아 state를 return !

const reducer = (state, action) => {
  // case, if문 등으로 분기 나눠서 처리 
  return state;
}

dispatch는 함수형 업데이트 필요 없이, 자동으로 reducer가 최신의 state를 가져와 사용함 !

최적화
10.2) useMemo와 연산 최적화

최적화 1 : 연산 결과 재사용 useMemo()

memoization = 이미 계산해본 결과를 기억해뒀다가 동일한 연산 수행 시 저장해둔 값을 가져다 쓰는 것  

 어떤 값을 반환하는 함수의 연산이 필요할 때만 실행되도록 하는 최적화 방법


	const getDiaryAnalysis = () => {
		console.log("일기 분석 시작");
		
		const goodCount = data.filter((it) => it.emotion >= 3).length;
		const badCount = data.length - goodCount;
		const goodRatio = (goodCount / badCount) * 100;
		
		return {goodCount, badCount, goodRatio};
	}
	
	const {goodCount, badCount, goodRatio} = getDataAnalysis();

-> 이 상태로 사용하면 일기 분석이 mount 시에 한 번, getData()로 가져온 데이터로 업데이트 시에 한 번 ... 이후 업데이트마다 매번 rerendering 되면서 계산하게 됨 !!

	const getDiaryAnalysis = useMemo(() => { // 첫 번째 인자로 들어오는 콜백 함수의 '값'을 메모이제이션
		console.log("일기 분석 시작");
		
		const goodCount = data.filter((it) => it.emotion >= 3).length;
		const badCount = data.length - goodCount;
		const goodRatio = (goodCount / badCount) * 100;
		
		return {goodCount, badCount, goodRatio};
	}, [data.length]); // data.length가 변화될 때만 콜백함수 재실행  
	
	// useMemo는 값을 반환하므로 이제 getDiaryAnalysis는 함수로 사용하는 게 아니라 값으로 사용 
	const {goodCount, badCount, goodRatio} = getDiaryAnalysis;
10.3) React.memo와 컴포넌트 렌더링 최적화

최적화 2 : 컴포넌트 재사용 React.memo

부모 컴포넌트가 rerender 되면 자식 컴포넌트도 불필요하게 리렌더링됨

-> 자식 컴포넌트에 각각의 업데이트 조건을 걸어줌

React.memo -> 고차 컴포넌트 ( 강화된 컴포넌트를 반환 ... )

동일한 props와 state -> 동일한 결과 반환하며, 리렌더링 x

const TextView = React.memo(({text}) => {
	useEffect(()=> {
		console.log(`update text ${text}`);
	})
	return <div>{text}</div>
}); // React.memo()로 컴포넌트를 감싸서 사용 ! 

원래는 둘 중 하나의 state만 변경되어도 하위 컴포넌트까지 모두 리렌더링됨

-> 따라서 React.memo로 렌더 낭비 방지 가능 !!

 

React.memo로 변경 후가 기존 값과 동일할 경우에 리렌더링 방지 가능 !

-> 값이 같아도 객체 생성으로 값 변경 시 객체가 새로 설정되며 주소가 다르게 판단됨 )

 

React.memo()는 두 번째 인자로 비교 함수를 받으므로 아래처럼 처리하면 객체도 비교 가능 !!

const areEqual = (prevProps, nextProps) => {
	if ( prevProps.obj.count === nextProps.obj.count ) {
		return true; // props 동일 -> rerender x 
	}
	
	return false // props 변경 -> rerender o
}

// 위에서 만든 비교 함수를 추가로 사용해서 객체 비교도 수월하게 ~ 
const CounterB = React.memo(({ obj }) => {
	useEffect(()=> {
		console.log(`update obj ${obj}`);
	})
	return <div>{ obj.count }</div>
});
10.4) useCallback과 함수 재생성 방지

컴포넌트 최적화

어떤 컴포넌트가 최적화 대상? -> 다양한 환경에서 React Dev Tools로 리렌더링 여부 확인

다이어리 삭제 시에도 editor가 리렌더링 ...

React.memo()를 감쌀 때 본문이 너무 길다면 export 시에 묶어도 ok

export default React.memo(DiaryEditor);

Editor가 props로 전달 받는 onCreate도 전체 App이 리렌더링 될 때 다시 전달되므로 리렌더링 됨 ( 비 원시 타입이므로 얕은 비교 진행됨 !! )

-> 처음에 data 배열이 빈 배열에서 데이터가 존재하는 배열로 변경될 때도 마찬가지이므로 처음에도 2번 렌더링...

-> 똑같은 이유로 data가 변경, 즉 삭제될 때도 onCreate가 재생성됨 ...

useCallback

첫 번째 인자로 callback 함수, 두 번째 인자로 dependency array를 받아 memoization된 함수를 반환함

즉 의존성 배열이 변경되지 않는 경우 callback 함수를 그대로 사용 ~!!

그런데 이때 빈 배열을 줘버리면 mount 시에만 함수가 생성되므로 setData를 할 때 최신 정보를 가져올 수 없음 ... -> 함수형 업데이트 ( set 함수에 함수를 전달 ) 이용!!

  const onCreate = useCallback((author, content, emotion) => {
    const created_date = new Date().getTime();
	  
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current
    };
	  
    // 개수 증가
    dataId.current += 1;
	// 함수형 업데이트로 최신 데이터 사용 가능하게끔 !! 
    setData((data) => [newItem, ...data]);
	  
  }, []); // 의존성 배열이 빈 배열이므로 mount 시에만 새로 ... 
Context
11.1) Context란

Props Drilling -> 자식에게 props를 보내기 위해 그냥 거쳐가기만 하는 prop들이 존재함 ...

  1. 모든 컴포넌트가 Provider에게 자신의 데이터를 전달

  2. Provider가 자신의 모든 자식에게 데이터를 직접 전달

    -> 데이터가 Provider 하에 주고받아지는 Context 속에서 컴포넌트들 존재 ...

  3. 당연히 Provider 외에 존재하는 컴포넌트는 Context 안에 포함되지 않고, 데이터 사용도 불가능 !

Context API를 통해 쉽게 작성 가능 !!

export const MyContext = React.createeContext(defaultValue);

// 데이터 공급
<MyContext.Provider value={전역으로 전달하고자 하는 값}>
    <자식 컴포넌트 />
</MyContext.Provider>
import React from 'react'

import { useReducer, useRef, useCallback, useEffect, useMemo } from 'react';

export 할 때 default로 export한 것들만 비구조화 할당 없이 바로 import 해올 수 있음 !!

Context의 값 사용하기

import React, { useContext } from 'react';
import { DiaryStateContext } from "./App";

const DiaryList = ({ onRemove, onEdit }) => {
    // console.log(diaryList);
	
	// Context에서 값 가져오기
	const diaryList = useContext(DiaryStateContext);
	
    return (
        <div className="DiaryList">
            <h2>일기 리스트</h2>
            <h4>{diaryList.length}개의 일기가 있습니다.</h4>
            <div>
                { diaryList.map((it) => (
                    <DiaryItem key={it.id} {...it} onRemove={onRemove} onEdit={onEdit} />
                ))}
            </div>
        </div>
    );
};

onCreate, onRemove 등의 함수는 단순히 value로 Provider에 전달해서는 안 됨 ...

Provider도 하나의 컴포넌트이므로 data가 바뀔 때마다 함수도 재생성된다 !!

-> Context를 중첩으로 사용해서 해결

	const memoizedDispatches = useMemo(() => {
		return { onCreate, onRemove, onEdit }
	}, []);
	
<DiaryStateContext.Provider value={ data }>
		  <DiaryDispatchContext.Provider value={ memoizedDispatches }>
			<div className="App">
			  {/* <Lifecycle /> */}
			  {/* <OptimizeTest /> */}
			  <DiaryEditor onCreate={onCreate} />
				  <div>전체 일기 : {data.length}</div>
				  <div>기분이 좋았던 일기 : {goodCount}</div>
				  <div>기분이 나빴던 일기 : {badCount}</div>
				  <div>기분 좋은 일기 비율 : {goodRatio}%</div>
			  <DiaryList onRemove={onRemove} onEdit={onEdit} />
			</div>
		  </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>

useMemo를 사용하는 이유 = App 컴포넌트 재생성 시에 재생성되는 것을 막음

11.3) Context 분리하기

일기 아이템 추가 / 삭제 시 다른 아이템도 모두 리렌더링됨 ... 아이템, 정보 양이 많을수록 큰 문제 !!

DiaryItem은 2가지의 함수, 6개의 data를 Props로 전달 받음 ... -> 변경되지 않는 정보를 위주로 최적화

onEdit과 onDelete의 경우는 onCreate와 마찬가지로 재생성이 불가피한 함수임 -> 함수 업데이트 이용

  const onRemove = useCallback(( targetId ) => {
	  // console.log(`${targetId}가 삭제되었습니다.`);
	  setData(data => data.filter((it) => it.id !== targetId));
  }, []);
	
	
  const onEdit = useCallback((targetId, newContent) => {
	  setData((data) =>
		data.map((it) => 
			// id가 일치하는 데이터를 찾아서 내용물만 바꿔줌! 일치하지 않을 경우엔 그대로 냅둠 ... 
			it.id === target.id ? {...it, content: newContent } : it
		)
	  );
  }, []);
프로젝트3. 감정 일기장
12.2) 페이지 라우팅 1. 소개

Page Routing

여러 개의 페이지 -> 서버에서 페이지를 다루는 방법, Page Routing을 알아야 함

router : 데이터의 경로를 실시간으로 지정해주는 역할을 하는 무언가

routing : 어떤 네트워크 내에서 통신 데이터를 보낼 경로를 선택하는 일련의 과정 -> 데이터의 경로( route ) 설정하는 행위 자체와 그런 과정을 모두 포함

Page Routing : 브라우저에서 uri로 웹 서버에게 요청하면 웹 서버는 해당 페이지의 웹 문서를 브라우저에게 보내줌 ! 어떤 경로의 요청에 어떤 페이지를 줄 것인지 결정하는 것 ...

Multipage Application( MPA )

보통 웹 서버에서 여러 개의 page routing...

페이지가 깜빡이면서 실제로 이동하게 됨 !

Single Page Application ( SPA ) & CSR

단일 페이지 어플리케이션 = page가 1개 !!

무슨 요청을 해도 똑같은 html 웹 문서를 반환함

-> react app이 브라우저 단에서 요청에 따라 알아서 페이지를 '업데이트'함( Client Side Rendering )

-> 서버 통신 시간이 없으므로 빠르게 전환되고, 데이터가 필요할 경우 데이터만 전달받게 됨