React(React.js 또는 ReactJS)는 자바스크립트 라이브러리로서, 사용자 인터페이스(User Interface, UI)를 만들기 위해 사용된다. 페이스북(현 메타)에 의해 제작되었으며 유지보수되고 있다. React는 SPA나 모바일 애플리케이션 개발에 사용될 수 있다. 대규모 또는 복잡한 리액트 애플리케이션 개발에는 보통 라우팅, API 통신 등의 기능이 요구되는데 리액트에서는 기본적으로 제공되지 않기 때문에 추가 라이브러리를 사용해야 한다.
사용자 정의 태그를 만드는 기술을 사용하며, 가상 돔(Virtual DOM)을 사용하여 우리가 원하는 페이지를 효율적이고 빠르게 브라우저에 렌더링해준다. 이때, 렌더링이란 React에서 컴포넌트를 호출하는 것을 의미한다.
UI를 구성하는 독립적이고 재사용 가능한 코드 조각(마크업, CSS, JS를 포함)이며, 이는 특정 기능이나 UI 요소를 캡슐화한다. 과거에는 클래스형 컴포넌트가, 최근에는 Hooks의 도입으로 함수형 컴포넌트가 주로 사용되고 있다. 함수형 컴포넌트는 클래스형 컴포넌트에 비해 더욱 간결하고 이해하기 쉬운 코드를 작성할 수 있게 한다.
객체지향설계의 SOLID 중 S, 즉 SRP(Single Responsibility Principle, 단일 책임 원칙)에 맞게 컴포넌트를 잘 설계하면 재사용성과 유지보수성을 높일 수 있다.
React 컴포넌트명은 대문자로 시작
React 컴포넌트는 일반 JavaScript 함수이지만, 이름은 대문자로 시작해야 한다.
JavaScript를 확장한 문법으로, JavaScript 파일을 HTML과 비슷하게 마크업을 작성할 수 있도록 해준다. React에서는 렌더링이 JSX의 순수한 계산이어야 하며, DOM 수정과 같은 부수 효과를 포함해서는 안된다.
부수 효과를 렌더링 연산에서 분리하기 위해서는
useEffect
로 감싸야 한다.
<div>
또는 <>,</>
(Fragment)를 사용할 수 있다. Fragment는 HTML 트리 구조에 포함되지 않고 그룹화 해준다.<div>
로 묶을 수 있지만 이를 남발할 경우 css스타일링의 불편하고, <table>
처럼 정해진 구조를 따라할 경우 문제가 생긴다. 따라서 <Fragment>
(<>
로도 사용 가능함)를 사용하는 것이 권장된다.
aria-*
,data-*
속성은 HTML에서와 동일하게-
(대시)를 사용하여 작성한다.
JSX는 React에 종속되는 개념?
JSX와 React는 서로 다른 별개의 개념으로, 종종 함께 사용되기도 하지만 독립적으로 사용할 수도 있다. JSX는 확장된 문법이고, React는 JavaScript 라이브러리이다.
Fragment를 사용해야 하는 경우
map()
사용 시, 각 요소를<>
로 묶으면 key를 사용할 수 없으므로, 이러한 경우 반드시<Fragment>
로 명시해야 한다.
export default function Avatar() {
return (
<img
className="avatar"
src="https://i.imgur.com/7vQD0fPs.jpg"
alt="Gregorio Y. Zara"
/>
);
}
export default function TodoList() {
const name = 'Gregorio Y. Zara';
return (
<h1>{name}'s To Do List</h1>
);
}
=
바로 뒤에 오는 속성export default function Avatar() {
const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
const description = 'Gregorio Y. Zara';
return (
<img
className="avatar"
src={avatar}
alt={description}
/>
);
}
style
속성에 객체를 전달한다.export default function TodoList() {
return (
<ul style={{
backgroundColor: 'black',
color: 'pink'
}}>
<li>Improve the videophone</li>
</ul>
);
}
? :
문법을 사용하여 조건에 따른 렌더링을 수행할 수 있다.if (isPacked) {
return <li className="item">{name} ✅</li>;
}
return <li className="item">{name}</li>;
return (
<li className="item">
{isPacked ? name + ' ✅' : name}
</li>
);
&&
: 조건이 참일 때 일부 JSX를 렌더링하거나, 그렇지 않으면 아무것도 렌더링하지 않을 때 사용될 수 있다.return (
<li className="item">
{name} {isPacked && '✅'}
</li>
);
리액트의 LifeCycle(생명주기)란 컴포넌트가 생성, 변경, 제거되는 사이클을 의미한다. 크게 마운트, 업데이트, 언마운트 단계로 나눌 수 있다.
appendChild()
DOM API를 사용하여 생성한 DOM 노드를 화면에 표시한다.다음 리액트 코드의 실행 순서에 대해 설명해주세요. [자료출처: 매일메일]
가상돔(Virtual DOM)이란 한마디로 ‘실제 DOM을 JS 객체 형태로 복제한 사본’이라고 할 수 있다. UI의 이상적인 또는 ‘가상’적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 ‘실제’ DOM과 동기화하는 프로그래밍 개념이다. 이는 웹 페이지의 구조를 효율적으로 업데이트하기 위해 사용한다.
비교(diffing) 알고리즘의 효율화
리액트는 O(n^3)의 복잡도를 가질 수 있는 트리 비교 문제를, 휴리스틱을 통해 O(n)으로 최적화하였다. 휴리스틱 알고리즘은 아래와 같이 크게 두 가지 가정을 두고 있다.
- 서로 다른 타입의 두 요소는 서로 다른 트리를 만든다: DOM 요소의 타입이 다르면 비교를 수행하지 않고, 해당 요소와 자식들을 모두 새로 생성한다. 타입이 다른 경우, 보통 완전히 다른 컴포넌트로 교체되는 상황이 많기 때문에 이러한 가정은 효율적이라고 볼 수 있다. 만약 동일한 타입의 요소라면, 동일한 내역은 유지하고 변경된 속성만 갱신한다.
- 개발자가 key prop을 통해 여러 렌더링 사이에 어떤 자식 요소가 변경되면 안되는지 표시할 수 있다: 같은 레벨의 자식들끼리 비교할 때 개발자가 부여한 key prop을 사용하여 요소를 식별한다. 이를 통해 리스트의 일부만 수정됐을 때 모든 아이템 요소들을 불필요하게 갱신하지 않고, 실제 변경된 요소만 감지하여 효율적으로 갱신된다.
시간에 따라 변화하는 데이터를 의미하며, 어떠한 컴포넌트에든 추가할 수 있고 필요에 따라 업데이트할 수도 있다. 일반 변수와 달리, 특정 함수 호출이나 코드 내의 특정 위치와 관련이 없다. 동일한 이름의 컴포넌트가 두 개 이상 존재할지라도 각 컴포넌트(복사본)의 state는 별도로 저장된다.
리액트는 같은 컴포넌트가 같은 자리에 렌더링되는 한 state를 유지한다. 리렌더링할 때 state를 유지하고 싶다면, 트리 구조가 같아야 한다.
// 리액트 공식문서 예시
import { useState } from 'react';
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField />
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
MyComponent
를 렌더링할 때마다 또 다른 MyTextField
함수가 만들어진다. 동일한 함수에서 다른 컴포넌트를 렌더링할 때마다 React는 그 아래의 모든 state를 초기화한다. 이런 문제를 피하려면 항상 컴포넌트를 중첩해서 정의하지 않고 최상위 범위에서 정의해야 한다.
JSX 태그에 전달하는 정보로, Props를 통해 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 수 있다. 즉, 컴포넌트 간 통신을 할 수 있게 해주는 것이다. props에는 객체, 배열, 함수를 포함한 모든 JavaScript 값을 전달할 수 있다. 따라서 props는 함수의 인수와 동일한 역할을 한다고 볼 수 있으며, React 컴포넌트 함수는 하나의 인자, 즉 props 객체를 받는다. 보통은 전체 props 자체를 필요로 하지는 않기에, 개별 props로 구조 분해 할당하여 사용한다.
undefined
이라면 디폴트 값으로 대체된다.GalleryBoardPostForm.defaultProps = {
isModifying: false,
};
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
children
이라는 prop으로 자식 컴포넌트를 받을 수 있다.import Avatar from './Avatar.js';
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
export default function Profile() {
return (
<Card>
<Avatar />
</Card>
);
}
props의 불변성
props는 ‘변경할 수 없다’라는 의미의 불변성을 지닌다. props는 읽기 전용 스냅샷으로, 렌더링할 때마다 새로운 버전의 props를 받는다. 상호작용과 같이 컴포넌트가 props를 변경해야 하는 경우, 부모 컴포넌트에 다른 props, 즉 새로운 객체를 전달하도록 요청해야 한다. 그러면 이전의 props는 버려지고, 결국 자바스크립트 엔진은 기존 props가 차지했던 메모리를 회수하게 된다. 따라서 이러한 경우에는 props를 변경하는 대신, state를 변경(set state)해야 한다.
리액트에서는 부모에서 자식(단방향)으로의 컴포넌트 간 데이터 전달이 일어나고, 이때 리액트의 속성인 prop
을 통해 데이터를 전달하게 된다. 따라서 부모의 n번째 자식 컴포넌트에게 부모에서 정의한 데이터를 전달하려면 모든 중간 자식 컴포넌트들의 prop에도 이 데이터를 전달해야 한다.
자식 컴포넌트 입장에서는 본인은 props를 사용하지도 않는데 그저 자신의 자식을 위해서 props를 받아서 넘겨주는 셈이다. 이 과정이 만약 여러 깊이를 거쳐 일어난다면, 해당 데이터가 어느 지점으로부터 왔는지, 데이터를 사용하는 곳이 어디인지 추적하기 힘들 것이다. 이와 더불어, 리액트는 props의 변경에도 리렌더링을 수행하므로 중간 자식 컴포넌트에서는 불필요한 렌더링을 수행할 수 밖에 없어진다.
// Slot/children 기반 컴포지션 예시
function Card({ title, children, footer }) {
return (
<section className="card">
<h2>{title}</h2>
<div>{children}</div>
<div>{footer}</div>
</section>
);
}
// 사용 예: 필요한 컴포넌트를 "직접 꽂아 넣기"
<Card
title="프로필"
footer={<Button>수정</Button>}
>
<ProfileSummary />
<ProfileDetails />
</Card>
하지만 이러한 방법은 데이터(상태) 접근 자체를 전역적으로 해결해 주지는 못하고, 깊이가 깊어질수록 여전히 “부모에서 만든 값을 자식이 쓰기 위해 구조를 맞춰 꽂아 넣어야 한다”는 제약이 있다. 깊이가 계속 깊어지는 시점이 전역 상태 관리가 필요한 때인 것이다.사용자 정의 태그의
onClick{}
과 같은 함수는 이벤트 리스너가 아닌 prop이다.
각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 추적하게끔 도우는 속성이다. 이는 배열 항목이 정렬 등으로 인해 이동하거나 삽입되거나 삭제될 수 있는 경우 중요하다. 재정렬로 인해 위치가 변경되더라도 key
는 React가 생명주기 내내 해당 항목을 식별할 수 있게 해준다. 즉, key
를 잘 선택하면 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리에 올바르게 업데이트 하는데 도움이 된다.
function Nav(props) {
const lis = []
for(let i=0; i<props.topics.length; i++) {
let t = props.topics[i];
lis.push(<li key={t.id}><a href={'/read/'+t.id}>{t.title}</a></li>)
// key에 각각의 id (여기서는 topics 배열의 순차적인 id값들)를 부여
}
return (
<nav>
<ol>
{lis}
</ol>
</nav>
)
}
...
const topics = [
{id:1, title:'html', body: 'html is ...'},
{id:2, title:'css', body: 'css is ...'},
{id:3, title:'js', body: 'javascript is ...'},
]
key가 될 수 있는 소스 예시
- 데이터베이스의 데이터: 고유 id 사용
- 로컬 생성 데이터: uuid와 같은 패키지 사용
- 중첩된 key
import { recipes } from './data.js';
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
{recipes.map(recipe =>
<div key={recipe.id}> {/* 각 배열에 key가 필요 */}
<h2>{recipe.name}</h2>
<ul>
{recipe.ingredients.map(ingredient =>
<li key={ingredient}>
{ingredient}
</li>
)}
</ul>
</div>
)}
</div>
);
}
import { recipes } from './data.js';
function Recipe({ id, name, ingredients }) {
return (
<div>
<h2>{name}</h2>
<ul>
{ingredients.map(ingredient =>
<li key={ingredient}>
{ingredient}
</li>
)}
</ul>
</div>
);
}
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
{recipes.map(recipe =>
// 컴포넌트를 추출할 때 추출된 JSX 외부에 key를 남겨두어야 함
// 재사용성과 가독성 향상
<Recipe {...recipe} key={recipe.id} />
)}
</div>
);
}
동적인 리스트를 만들 때는 index값을 key로 전달하면 안된다. 요소들을 불러올 때 처음부터 고유한 id를 갖는 객체 배열로 받아와서 item.id
과 같이 key를 지정한다.
key로 Index를 사용해도 되는 경우
배열에 추가, 수정, 삭제가 일어나지 않는 경우
<input>
: value 속성을 통해 자체적인 데이터를 가지며, 사용자가 입력한 값이 value 속성에 저장된다. 이떄, value 속성은 DOM에 존재하므로, input을 통한 사용자의 입력 데이터는 DOM에 저장된다고 볼 수 있다.제어 컴포넌트 | 비제어 컴포넌트 | |
---|---|---|
특징 | 리액트가 값을 관리 | DOM이 값을 저장 |
사용자의 입력이 항상 state로 push | 입력 값이 필요할 때, element에서 pull | |
리액트가 값이 항상 일치함을 보장 | 값이 항상 일치함을 보장하지 않음 | |
리렌더링 발생 O | 리렌더링 발생 X | |
단점 | 리렌더링 이슈 존재 / 모든 form 요소에 react의 state를 연결해야 함 / non-React 코드로 작성된 form 요소 코드 통합의 어려움 |
리액트 상태(state)를 통해 입력 값을 제어하는 컴포넌트이며, 상태를 신뢰 가능한 단일 출처로써 사용한다. 항상 최신의 값을 보장하며, 매 입력마다 리렌더링이 발생한다. 따라서 매 입력마다 입력 값을 특정 동작을 수행해야 하는 경우 유용하다. 예를 들어, 입력값을 다른 곳에 렌더링하는 경우, 사용자 입력에 대한 즉각적인 유효성 검증을 해야 하는 경우가 있겠다.
원하는 시점에 값을 가져오며, 매 입력마다 리렌더링이 발생하지 않는다. 예를 들어, 매 입력마다 최신의 값이 꼭 필요하지 않은 경우, 매 렌더링마다 복잡한 연산이 발생하는 경우가 있겠다.
커스텀 훅은 이름이 use
로 시작하는 자바스크립트 함수이다. 다른 리액트 훅을 호출할 수 있으며, 특별한 기능이라기보다 기본적으로 Hook의 디자인을 따르는 관습이라 할 수 있다.
리액트에서 Strict Mode(엄격 모드) 는 주로 개발 중에 발생할 수 있는 잠재적인 문제를 사전에 감지하고 예방하기 위해 사용된다.
Strict Mode에서 코드가 두 번씩 실행되는 현상은 개발 모드에서만 발생하고, 실제 프로덕션 빌드에서는 발생하지 않는다.
attendancerank
는 데이터로 넘어온 이차원 배열을 한 번 까야지 나오는 데이터이다. 두 개의 주간/월간 출석자 배열 중 하나만 빈 배열일 수도 있다. 따라서, 무난하게 삼항연산자를 활용하였다.
const AttendanceBannerContent = ({ slideIdx }) => {
const {
data: {
weeklyStatisticsDtoList: weeklyAttendanceRank = [],
monthlyStatisticsDtoList: monthlyAttendanceRank = [],
} = {},
} = useGetAttendanceRanks();
return (
<>
{[weeklyAttendanceRank, monthlyAttendanceRank]?.map((attendanceRank, idx) => (
<div
key={idx}
className={`RankSlide absolute left-0 top-0 h-full w-full ${
slideIdx === idx + 1 ? 'visible' : 'invisible'
}`}
>
{attendanceRank.length === 0 ? (
<p className='w-full pt-10 text-center text-xs text-stone-500'>출석자가 없습니다</p>
) : (
<ul className='w-full px-4 text-black'>
{attendanceRank.map((ranker, idx) => (
<li key={idx} className='mb-1 flex h-4 w-full flex-row text-xs'>
<span className='mr-16 w-2 pl-2'>{idx + 1}</span>
</li>
))}
</ul>
)}
</div>
))}
</>
);
};
조건을 테스트하기 위해 JavaScript는 자동으로 왼쪽을 부울로 변환한다. 하지만 왼쪽이 0이면 전체 식이 0이 되고, React는 아무것도 아닌 0을 렌더링할 것이다. 실제로, 이러한 코드 상의 실수를 인턴 수행 중 수행했던 프로젝트 코드를 수정하면서 직접 확인할 수 있었다. 이전 작성자가 이 부분을 해결하려고 삼항연산자로 거짓일 때는 빈 문자열(’’)로 처리를 해놨는데, 이 부분을 간과한게 아닐까 추측한다. 실수하기 좋은 부분이니 잘 숙지하고 넘어가자.
// 잘못된 예시
messageCount && <p>New messages</p>
// 올바른 예시
messageCount > 0 && <p>New messages</p>
Suspense는 일반적으로 컴포넌트 스트리밍에 쓰인다. 아래 예시와 같이 쿼리 파라미터가 변하는 경우 로딩 상태를 표시하기 위해 Suspense를 사용한 경우를 생각해보자. 예상한 것과 달리 resolvedSearchParams의 q가 변하더라도 폴백 UI가 표시되지 않는다. 왜 이런 현상이 발생하는 것일까?
key 없이 사용하면 이는 q 값의 변경이 일어나도 Suspense 컴포넌트는 변경사항을 감지할 방법이 없기 때문이다. 따라서 Suspense 컴포넌트의 key에 쿼리 파라미터 값을 넘김으로써, 리액트에게 해당 값이 변하면 Suspense도 변경되어야 하는 것을 알리며 로딩 상태를 다시 보여줄 수 있다. 즉, 일종의 트릭을 사용하여 검색어가 변해서 결과를 기다리는 시간 동안에도 로딩 상태를 보여줄 수 있는 것이다.
// 이정환님의 한 입 크기로 잘라먹는 Next.js 중
// 검색창에 서로 다른 검색어를 입력할 때 실시간으로 목록을 갱신하는 상황을 구현
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const resolvedSearchParams = await searchParams;
return (
<Suspense key={resolvedSearchParams.q} fallback={<div>로딩중...</div>}>
{/* SearchResult: 비동기 fetching 진행 */}
<SearchResult q={resolvedSearchParams.q || ""} />
</Suspense>
);
}
// 단순히 일정한 개수의 사진 데이터를 불러와 보여줌
{photos.map((photo, idx) => (
<div key={idx} className='ImageCell inline-block h-fit w-[20rem] px-2'>
<img
className='brightness-98 rounded-sm shadow-md'
src={StringCombinator.getImageURL(photo.saveFilePath, photo.saveFileName)}
alt={'포토존 사진'}
/>
</div>
))}
도움되는 사이트
1. 컴포넌트 만들기 · GitBook
리액트 강의
도움되는 영상
React.Fragment는 무엇? 리액트 개발자라면 꼭 알아야됨 - 유튜브 별코딩
Virtual DOM과 Internals – React
재조정 (Reconciliation) – React
React의 가상돔 (Virtual DOM)이 뭔가요? (짱 쉬움)
[10분 테코톡] 텐텐의 리액트의 렌더링
매일메일 - Virtual DOM에 대해서 설명해주세요.
[10분 테코톡] 프룬의 리액트 Props Drilling
React - List와 Key의 중요성. 디버깅의 악몽을 피하자!
[10분 테코톡] 후이의 제어 컴포넌트 vs 비제어 컴포넌트
[10분 테코톡] 세인의 제어 컴포넌트와 비제어 컴포넌트
커스텀 Hook으로 로직 재사용하기 – React
[10분 테코톡] 헤일리의 Custom Hook
참고자료
React