[React] 3. 함수 컴포넌트 (Functional Component)
함수 컴포넌트 (Functional Component) : 리액트 훅을 통한 상태와 생명주기 관리
클래스 컴포넌트의 한계?
- 데이터의 흐름을 추적하기 어려움 : 서로 다른 여러 메소드에서, 작성 순서에 상관없이 상태의 업데이트 발생 가능
- 어플리케이션 내부 로직의 재사용이 어려움 : 공통 로직이 많아질수록 이를 감싸는 고차 컴포넌트나
props이 많아짐- 기능이 많아질수록 컴포넌트의 크기가 커짐 : 내부에서 처리하는 데이터 흐름이 복잡해짐
import { useState, useEffect } from 'react';
const MyFunctionalComponent = () => {
// useState 훅을 사용하여 상태 정의
const [count, setCount] = useState(0);
// useEffect 훅을 사용하여 부수 효과(라이프사이클 작업 등) 처리
useEffect(() => {
console.log('Component mounted or count updated:', count);
// componentWillUnmount 역할을 하는 함수 (클린업 함수)
return () => {
console.log('Component will unmount');
};
}, [count]); // count가 업데이트될 때만 실행
const handleIncrement = () => {
// setCount를 사용하여 상태 업데이트
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
};
export default MyFunctionalComponent;
useState,useEffect,useContext와 같은 리액트 훅을 사용하여 상태를 관리useEffect훅을 통해 라이프사이클 메소드와 유사한 동작을 수행 가능this키워드를 사용하지 않고, 클래스 컴포넌트보다 간결하고 명료하게 작성 가능
리액트 훅 (React Hook) : 함수형 컴포넌트에서 상태와 생명주기 메서드를 사용할 때 사용
- 상태 관리 훅 :
useState,userContext,useReducer - 시점 훅 :
useLayoutEffect,useEffect - 메모이제이션 (
Memo) 훅 :useMemo,useCallback,memo
훅의 규칙 (
Rules-of-Hooks) : 관련ESLint규칙으로react-hooks/rules-of-hooks또한 존재
(1)최상위에서만 훅을 호출해야 한다. (반복문, 조건문, 중첩 함수 내에서 훅을 실행할 수 없다.)
→ 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
(2)훅을 호출할 수 있는 것은 함수 컴포넌트, 혹은 커스텀 훅만 가능하다.
useState : 함수형 컴포넌트 내부에서 상태를 정의하고 관리할 때 사용
import { useState } from 'react'
const [state, setState] = useState(initState)
- 인수로 사용할 상태의 초기값을 받음
- 첫번째 값에 현재 상태의 값, 두번째 값에 상태를 업데이트하는 함수가 담긴 배열을 반환
const [state, setState] = useState(() => Number.praseInt(window.localStorage.getItem(cacheKey)));
- 게으른 초기화 (
Lazy Initialization) :useState등 리액트 훅의 인자에 변수 대신 함수 자체를 넘기는 것useState의 초기값이 복잡하거나 무거운 연산을 포함하는 경우에 사용- 오직 상태가 처음 만들어질 때만 사용 → 리렌더링이 발생하면 이 함수의 실행은 무시
import { useState } from 'react';
const Component = () => {
// useState를 사용하여 counter라는 상태를 정의하고 초기값을 0으로 설정
const [counter, setCounter] = useState(0)
const handleClick = () => {
setCounter((prev) => prev + 1)
}
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
useEffect : 어플리케이션 내 컴포넌트의 여러 값들을 통해 부수 효과를 만들 때 사용
const Component = () => {
useEffect(
() => {},
[props, state]
)
}
- 첫번째 인수로 콜백 함수, 두번째 인수로 의존성 배열을 받음 : 의존성 배열의 값이 변경되면 콜백 실행
componentDidMount시점에 비동기로 실행 →rendering이후,mount이후
- 클래스 컴포넌트의 생명주기와 비슷한 동작을 구현 가능 : 의존성 배열에 빈 배열을 넣으면 컴포넌트가 마운트될 때만 실행
- 렌더링이 실행될 때마다 의존성에 있는 값을 보면서 값 (
state,props)이 변경되면 부수 효과를 실행
const Component = () => {
const [counter, setCounter] = useState(0)
const handleClick = () => {
setCounter((prev) => prev + 1)
}
const counter = 1
useEffect(
() => {
console.log(count) // 1, 2, 3, 4...
}
)
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
- 클린업 함수를 반환 가능 : 컴포넌트가 언마운트될 때 실행 (
componentWillUnMount)- 함수 컴포넌트가 리렌더링되었을 때 의존성 변화가 있었을 당시의 값 기준으로 실행되어 이전 상태를 청소
- 함수 컴포넌트의
useEffect는 콜백이 실행될 때마다 이전 클린업 함수가 존재하면 그 클린업 함수를 실행한 뒤 콜백 실행- 특정 이벤트 핸돌러가 무한히 생성되는 것을 방지
import { useState, useEffect } from 'react'
export default function App() {
const [counter, setCounter] = useState(0)
const handleClick = () => {
setCounter((prev) => prev + 1);
}
useEffect(() => {
const addMouseEffect = () => {
console.log(counter)
}
window.addEventListener('click', addMouseEffect);
return () => {
console.log('클린업 함수', counter);
window.removeEventListener('click', addMouseEffect);
}
}, [counter]);
return (
<>
<h1>{counter}</h1>
<button onclick={handleClick}>+</button>
</>
)
}
// 클린업 함수 0
// 1
// 클린업 함수 1
// 2
// ...
- 의존성 배열 (
dependency arra) : 내부의 특정 값이 변경될 때에만useEffect콜백이 실행되도록 제어- 의존성 배열에 사용자가 원하는 값을 넣는 경우 : 해당 값이 변경될 따마다 실행
- 의존성 배열에 빈 배열을 두는 경우 : 최초 렌더링 직후에 한번 실행하고 더 이상 실행하지 않음
- 의존성 배열에 아무런 값을 두지 않는 경우 : 렌더링이 발생할 때마다 매번 실행
vs직접 실행? : 클라이언트 사이드에서 실행 보장, 컴포넌트 렌더링 완료 이후에 실행
useEffect(() => {
// 실행될 코드
}, [dependency1, dependency2, ...]); // 의존성 배열
useMemo : 큰 연산에 대한 결과를 메모이제이션 (Memoization)할 때 사용
import { useMemo, useState } from 'react'
const ExpensiveComponent = ({ value }) => {
useEffect(() => {
console.log('rendering')
})
return <span{value + 10000}></span>
}
const App = () => {
const [value, setValue] = useState(10);
const [, triggerRendering] = useState(false);
const MemoizedComponent = useMemo(
() => <ExpensiveComponent value={value}>, [value]
)
const handleChange = (e) => {
setValue(Number(e.target.value))
}
const handleClick = () => {
setValue(Number(e.target.value))
}
return (
<>
<input value={value} onChange={handleChange}></input>
<button = onClick={handleClick}>렌더링 발생</button>
{MemoizedComponent}
</>
)
}
- 첫번째 인수로 생성 함수, 두번째 인수로 해당 함수가 의존하는 값의 배열을 전달
- 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면, 함수를 재실행하지 않고 이전의 값을 반환
- 렌더링 발생 시 의존성 배열의 값이 변경되었으면, 첫번째 인수의 함수를 실행한 후에 그 값을 반환 및 메모이제이션
React.Memo : 외부 상태나 컨텍스트에 의존하지 않고 순수 함수형 컴포넌트를 메모이제이션할 때 사용
const CreateUser = React.memo(({ username, email, onChange, onCreate }) => {
return (
<div>
<input
name="username"
placeholder="계정명"
onChange={onChange}
value={username}
/>
<input
name="email"
placeholder="이메일"
onChange={onChange}
value={email}
/>
<button onClick={onCreate}>등록</button>
</div>
);
});
- 컴포넌트의
props이 바뀔 때에만 리렌더링을 수행
useCallback : 인수로 넘겨받은 콜백 자체를 저장해, 재성성 대신 재사용할 때 사용
const ChildComponent = memo(({name, value, onChange}) => {
useEffect(
() => {
console.log('rendering', name)
}
)
return(
<>
<h1>{name} (value ? '켜짐' : '꺼짐')</h1>
<button onClick={onChange}>toggle</button>
</>
)
})
const App = () => {
const [status1, setStatus1] = useState(false)
const [status2, setStatus2] = useState(false)
const toggle1 = useCallback(
function toggle1() {
setStatus(!status1)
}, [status1],
)
const toggle2 = useCallback(
function toggle2() {
setStatus(!status2)
}, [status2],
)
return (
<>
<ChildComponent name="1" value={status1} onChange={toggle1}></ChildComponent>
<ChildComponent name="2" value={status2} onChange={toggle2}></ChildComponent>
</>
)
}
- 첫번째 인수로 함수, 두번째 인수로 해당 함수가 의존하는 값의 배열을 전달
- 값의 메모이제이션을 위해
useMemo를 사용했다면, 함수의 메모이제이션을 위해 사용하는 것이useCallback!- 해당 의존성이 변경되었을 때만 함수가 재생성 : 불필요한 리소스 및 리렌더링 방지 가능
useMemo을 통해useCallback를 구현할 수 있음
useRef : 함수형 컴포넌트 내에서 참조를 생성하고 관리할 때 사용
import { useRef, useEffect } from 'react';
function MyComponent() {
const myInputRef = useRef(null);
useEffect(() => {
// 컴포넌트가 마운트된 후, input 요소에 포커스를 줌
myInputRef.current.focus();
}, []);
return <input ref={myInputRef} />;
}
- 반환값인 객체 내부에 있는
.current로 값에 접근 혹은 변경이 가능 - 컴포넌트의 렌더링과 관계없이 변수를 저장 가능 →
useState와 달리, 매번 리렌더링이 일어나지 않음 - 주로
DOM요소에 접근하거나 컴포넌트의 생명주기와 독립적으로 값을 유지하는 상황에서 활용
import { useRef, useState, useEffect } from 'react';
function MyComponent() {
const countRef = useRef(0);
const [count, setCount] = useState(0);
useEffect(() => {
// countRef는 렌더링과 무관하게 유지되는 변수
countRef.current = count;
}, [count]);
const handleClick = () => {
setCount(count + 1);
console.log(countRef.current); // 항상 가장 최근 값 출력
};
return <button onClick={handleClick}>증가</button>;
}
useContext : React Context를 통해 전역으로 상태를 공유하거나 전달할 때 사용
prop내려주기 :A컴포넌트가 제공하는 데이터를D컴포넌트에서 사용하려면, …- 하위 컴포넌트로 필요한 위치까지 계속해서 넘겨주어야 함 → 제공하는 쪽, 제공받는 쪽 모두 번거로운 작업!
<A props={props}>
<B props={props}>
<C props={props}>
<D props={props}>
...
</D>
</C>
</B>
</A>
- 리액트 컨텍스트 (
React Context) : 컴포넌트 트리 안에서 전역적으로 데이터를 공유- 중첩 컴포넌트 간에 데이터를 명시적으로 전달하지 않아도 되므로,
Props를 여러 단계에 걸쳐 전달하지 않아도 됨
- 중첩 컴포넌트 간에 데이터를 명시적으로 전달하지 않아도 되므로,
// 1. Context 객체 생성
const MyContext = React.createContext(defaultValue);
// 2. Context를 제공하는 컴포넌트 작성
const MyContextProvider = ({ children }) => {
const contextValue = // ... (상태 값이나 함수 등)
return (
<MyContext.Provider value={contextValue}>
{children}
</MyContext.Provider>
);
};
// 3. useContext를 사용하여 값에 접근
import { useContext } from 'react';
const MyComponent = () => {
const contextValue = useContext(MyContext); // 이때, 리액트가 아닌 자비스크립트가 반환된다.
// contextValue를 사용하여 렌더링 또는 다른 로직 수행
};
Context.Provider: 컨텍스트 값을 하위 컴포넌트에 제공value속성을 통해 전달할 값을 설정
const MyContextProvider = ({ children }) => {
const contextValue = // ... (상태 값이나 함수 등)
return (
<MyContext.Provider value={contextValue}>
{children}
</MyContext.Provider>
);
};
useReducer : 복잡한 상태 로직을 다룰 때 사용
// useReducer가 사용할 State 정의
type State = {
count: number
}
// State의 변화를 발생시킬 Action의 타입과 넘겨줄 payload 정의
type Action = {
type: 'up' | 'down' | 'reset';
payload?: State;
}
// 무거운 연산이 포함된 게으른 초기화 함수 init 정의
const init = (count: State) => {
return count
}
const initState: State = { count: 0 }
const reducer = ((state: State, action: Action): State) => {
switch (action.type) {
case 'up':
return { count: state.count + 1 }
case 'down':
return { count: state.count - 1 > 0 ? state.count - 1 : 0 }
case 'down':
return init(action.payload || { count: 0 })
default:
throw new Error(`Unexpected Action type: ${action.type}`)
}
}
const export default App = () => {
const [state, dispatcher] = useReducer(reducer, initialState, init)
const handleUpButtonClick = () => {
dispatcher({ type: 'up' })
}
const handleDownButtonClick = () => {
dispatcher({ type: 'down' })
}
const handleResetButtonClick = () => {
dispatcher({ type: 'reset', payload: { count: 1 } })
}
return {
<div className="App">
<button onclick={handleUpButtonClick}>+</button>
<button onclick={handleDownButtonClick}>-</button>
<button onclick={handleResetButtonClick}>Reset</button>
</div>
}
}
- 반환값은
useState와 동일하게 길이가2인 배열state: 현재useReducer가 갖고 있는 값dispatcher:state를 업데이트하는 함수
(값만 넘겨주는setState와 달리,state를 변경할 수 있는action반환)
- 인수는
useState와 달리 2~3개의 인수를 필요로 함reducer: 첫번째 인수 →useReducer의 기본action을 정의하는 함수initialState: 두번째 인수 →useReducer의 초깃값init: (선택) 세번째 인수 → 초깃값을 지연해서 생성 (게으른 초기화)
useReducer의 목적?
(1)복잡한 형태의state를 사전에 정의된dispatcher로만 수정할 수 있게 하여,(2)state값에 대한 접근은 컴포넌트에서만 가능하게 하고,(3)이를 업데이트하하는 방법에 대한 상세 정의 컴포넌트 밖에 둔 다음,(4)state의 업데이트를dispatcher로 제한한다!
state하나가 가질 값이 복잡하고 이를 수정하는 경우의 수가 많아지면state를 관리하는 것이 어려워짐- 여러
state를 관리하는 것보다 성격이 비슷한 것들을 묶어useReducer로 관리하는 것이 효율적- 게으른 초기화 함수를 인자로 사용하여
useState에 함수를 넣은 것과 같은 이점을 누릴 수 있음- 추가로
state에 대한 초기화가 필요할 때reducer에서 재사용할 수 있음
forwardRef : 부모 컴포넌트에서 자식 컴포넌트로 ref를 전달할 수 있게 함
import { useRef, forwardRef } from 'react';
// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
const internalState = useRef(null);
// 부모 컴포넌트에서 전달한 ref에 직접 접근
// ref.current를 통해 부모 컴포넌트의 ref를 참조할 수 있음
const handleButtonClick = () => {
console.log('자식 컴포넌트에서 버튼 클릭');
console.log('내부 상태:', internalState.current);
};
return (
// 여기에 컴포넌트 JSX를 작성
<div>
<button onClick={handleButtonClick}>자식 컴포넌트에서 클릭</button>
</div>
);
});
// 부모 컴포넌트
const ParentComponent = () => {
// ref를 생성하여 자식 컴포넌트에 전달
const childRef = useRef();
// 부모 컴포넌트에서 ref를 자식 컴포넌트에 전달
return (
<div>
{/* forwardRef를 사용하여 자식 컴포넌트에 ref 전달 */}
<ChildComponent ref={childRef} />
</div>
);
};
export default ParentComponent;
ref를 받고자 하는 컴포넌트를forwards로 감싸고, 두번째 인수로ref전달- 부모 컴포넌트에서
props.ref를 통해ref를 전달
- 부모 컴포넌트에서
useRef에서 반환된ref객체를 상위에서 하위 컴포넌트로 전달할 때, 직접props를 넣을 수 없을 때 사용- 컴포넌트 외부의
DOM에 접근 : 부모에서 선언 → 자식에 전달 → 자식에 참조 걸기 → 부모에서 컨트롤
- 컴포넌트 외부의
But, 외부DOM을 참조하는ref를 갖는 것은Coupling이 강해지므로 좋지 않다!
useImperativeHandle (with forwardRef) : 부모 컴포넌트가 자식 컴포넌트를 컨트롤할 수 있게 함
import { forwardRef, useRef, useImperativeHandle } from 'react';
const ChildComponent = forwardRef((props, ref) => {
// useImperativeHandle을 사용하여 부모 컴포넌트에 특정 함수나 값들을 노출
useImperativeHandle(ref, () => ({
// 부모 컴포넌트에서 입력 엘리먼트에 포커스를 맞추기 위해 함수를 노출
focusInput: () => {
inputRef.current.focus();
}
}));
const inputRef = useRef(null);
return <input ref={inputRef} />;
});
const ParentComponent = () => {
const childRef = useRef(null);
const handleClick = () => {
// 자식 컴포넌트에서 노출한 함수를 호출하여 입력 엘리먼트에 포커스를 맞춤
childRef.current.focusInput();
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>입력에 포커스 맞추기</button>
</div>
);
};
export default ParentComponent;
useImperativeHandle을 통해 부모 컴포넌트에 함수 및 기능을 노출시킴- 부모에게 참조값을 전달 → 자식은 객체를 반환
HTMLElement만 주입 가능한ref에, 자식 컴포넌트에 새로 설정한 객체의 키·값에 대해서도 접근 가능
useLayoutEffect : 시그니처가 useEffect와 동일하나, 모든 DOM 변경 후의 콜백 실행이 동기적으로 발생
import React, { useLayoutEffect, useState } from 'react';
const MyComponent = () => {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
// DOM 요소의 너비 측정
const element = document.getElementById('myElement');
if (element) {
const elementWidth = element.clientWidth;
setWidth(elementWidth);
}
}, []); // 빈 의존성 배열은 이 효과가 초기 렌더 이후에 한 번만 실행되도록 함
return (
<div>
<p>내 요소의 너비는: {width}px</p>
<div id="myElement">이것은 내 요소입니다</div>
</div>
);
};
export default MyComponent;
- ‘시그니처가
useEffect와 동일하나,’ : 두 훅의 형태나 사용 예제가 동일함 - ‘모든
DOM변경 후의 콜백 실행이 동기적으로 발생’ :(1)리액트가DOM을 업데이트 →(2)useLayoutEffect실행 →(3)useEffect실행 DOM은 계산되었지만, 이것이 화면에 반영되기 전에 하고 싶은 작업을 처리할 때 사용
사용자 정의 훅 (Custom Hook) : 서로 다른 컴포넌트 내부에서 같은 로직을 공유할 때 사용
- 기존에 존재하는 훅을 기반으로 필요한 훅을 개발 (함수 이름이 반드시
use로 시작)- 사용자 정의 훅의 동작, 매개변수, 반환값을 사전에 정의한 상태에서 구현하여 다른 컴포넌트에서 활용