Contents
Describing the UIDescribing the UIThe Concepts of ComponentsImporting and Exporting ComponentsWriting Markup With JSXJavaScript in JSX with Curly BracesPassing Props to a ComponentConditional RenderingRendering ListsKeeping Components PureUnderstanding Your UI as a Tree
Describing the UI
The Concepts of Components
Chakra UI 와 Material UI와 같은 리액트 커뮤니티에서 미리 만들어진 수천 개의 컴포넌트를 사옹하여 프로젝트 진행 속도를 높일수 있다.
웹 페이지를 만드는 전통적인 방식은 컨텐츠를 마크업하고 그다음 자바스크립트를 뿌려sprinkle 상호작용을 추가했다. React는 이와 동일한 기술을 사용하면서 상호작용성을 우선한다(React 컴포넌트는 마크업을 뿌려넣을 수 있는 자바스크립트 함수다).
React 컴포넌트는 일반 자바스크립트 함수지만 함수명이 반드시 대문자로 시작해야 한다. 대문자로 시작하지 않으면 작동하지 않는다.
컴포넌트가 다른 컴포넌트를 렌더하는 것(부모-자식 요소 관계)는 가능하지만 컴포넌트 내부에서 다른 컴포넌트를 정의해서는 절대 안된다. 모든 컴포넌트는 탑 레벨에서 정의돼야한다.
React는 빈 HTML 파일과 페이지 운영을 React가 자바스크립트를 사용해 전담하는 방식을 사용한다. Next.js와 같은 React 기반 프레임워크는 여기서 더 나아가 React 컴포넌트에서 HTML이 자동 생성한다. 이를 통해 자바스크립트 코드 로드 이전에 일부 컨텐츠를 먼저 표시되게 할 수 있다.
Importing and Exporting Components
컴포넌트 import, export는 default와 named 방식 모두 가능하다.
default 와 named 방식의 혼용이 가져오는 혼선의 가능성을 고려해 둘 중 하나의 방식만 사용하거나 하나의 파일에선 하나의 방식만을 사용하고 혼합하지 않는 스타일을 사용할 수도 있다.
Writing Markup With JSX
고전적 웹페이지 제작에서 컨텐츠는 HTML에 작성되고 상호작용을 위한 로직은 별도의 자바스크립트에 작성되었다. 그러나 웹이 더 상호작용적이게 됨에따라, 로직이 컨텐츠를 확정하게 되었다. 다시 말해, 자바스크립트(로직)이 HTML의 역할까지 담당하게 된것이다. 이것이 React에서 렌더링 로직과 마크업이 ‘컴포넌트’라는 같은 곳에 위치하게된 이유다.

렌더링 로직과 마크업이 함께 컴포넌트에 있으면서 컴포넌트 수정시 로직과 마크업 간의 동기화된 상태를 보장한다. 반면, 서로 무관한 마크업은 다른 컴포넌트로 분리됨에 따라 컴포넌트의 변경의 다른 컴포넌트에 영향을 주지 않아 더 안전하게 수정할 수 있다.
JSX는 React는 매우 자주 같이 사용되는것과 별개로 별도의 요소임을 기억하자. 당연히 각각 따로 사용할 수 있으며 React는 자바스크립트 라이브러고, JSX는 자바스크립트의 문법을 확장한 것이다.
JSX는 HTML과 거의 똑같이 생겼지만 좀 더 엄격한 문법을 요구하며 동적 정보를 표시할 수 있는 차이가 있다. 그 내용은 아래와 같다.
- 단일 요소를 반환해야 한다. 이때 요소를 감싸는 부모 태그로서
<></>
형태의 Fragment를 사용할 수 있다. Fragment는 브라우저 HTML 트리에 어떤 흔적도 남기지 않으면서 자식 요소를 그룹핑해 단일 요소로 만들 수 있다. - JSX는 HTML 처럼 생겼지만 결과적으로 바닐라 자바스크립트 객체로 변환된다. 자바스크립트 함수는 두 개 이상의 객체를 그대로 반환할 수없다. 각 객체를 요소로 하나의 배열로 감싸야만 한다. 이것이 두 개 이상의 요소를 반환하려면 Fragment같은 부모 요소로 감싸야하는 이유다.
- 모든 태그를 닫아야한다. HTML과 다르게
<img>
를 허용하지 않으며<img />
로 모든 태그는 명시적으로 닫아줘야한다.
- ’거의’ 모든걸 카멜케이스로 작성한다. JSX는 자바스크립트 객체로, JSX의 속성은 객체의 키로 변환된다. 그리고 많은 경우 JSX 속성은 해체 할당되어 변수로 사용된다. 그래서 React안 HTML과 SVG 속성 상당수가 자바스크립트 변수명 제한(대시 포함 불가, 예약어 사용 불가)을 고려하여 카멜케이스로 변경되었다(예:
stroke-width
→strokeWidth
). - 관습을 이유로 aria-* 와 data-* 속성은 HTML과 같이 대시와 함께 작성된다.
- 기존 HTML 마크업을 JSX로 바꿔야 한다면 converter 를 사용하자.
JavaScript in JSX with Curly Braces
중괄호는 JSX 마크업 안에서 자바스크립트를 쓰려고 할 때 사용한다. 중괄호 안에는 모든 자바스크립트 표현식이 올 수 있으며, 함수 호출 역시 값으로 평가되는 표현식이므로 중괄호안에 올 수 있다.
JSX에서 중괄호는 아래 두 가지 용도로만 사용할 수 있다.
- JSX 태그 바로 안쪽 텍스트. 태그 안에서 평가되어 텍스트를 채우는 용도. 태그 안 텍스트가 아닌 태그명에 중괄호 사용 불가.
<{tag}>Gregorio Y. Zara's To Do List</{tag}>
는 동작하지 않는다.
- 속성 바로 옆에있는
=
에 할당하는 경우.src={avatar}
와 같이 사용한다.src="{avatar}"
는 문자열"{avatar}"
로 인식한다.
Passing Props to a Component
React 컴포넌트간 소통을 위해 props 를 사용한다. props을 통해 함수, 배열, 객체를 포함한 모든 자바스크립트 값을 전달할 수 있다.
커스텀 컴포넌트가 아닌
<img>
와 같은 JSX 태그의 props는 className
, src
, alt
, width
와 같이 미리 정해져 있으며 해당 요소의 정보를 전달하는 용도다. 반면 커스텀 컴포넌트에는 어떤 props도 전달할 수 있다.React 컴퍼넌트 함수는
props
라는 객체 하나만을 인수로 받는다. 즉, 컴포넌트에 건내는 모든 props이 props
객체의 프로퍼티가 된다.function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}
보통은
props
객체를 아래처럼 해체할당하여 컴포넌트 내부에서 변수처럼 사용한다.function Avatar({ person, size }) {
// 이제 person, size를 값으로 평가되는 식별자로
// 컴포넌트 안에서 사용할 수 있다.
}
prop의 기본 값은 아래와 같이 지정할 수 있다.
function Avatar({ person, size = 100 }) {
// ...
}
기본 값이 지정된 size에 undefined를 할당하면
size={undefined}
기본 값이 사용되지만 null size={null}
또는 0을 size={0}
을 할당하면 기본 값은 사용되지 않는다. 부모 컴포넌트의 props를 그대로 자식 컴포넌트에 넘길 땐 간결하게 아래와 같이 spread 문법을 사용한다.
// Avatar 컴포넌트의 속성은 결과적으로 객체를 받는 인자인 props의
// 프로퍼티다. 즉, 부모 컴포넌트의 props 객체를 자식 컴포넌트의 빈
// props 객체에 풀어 할당하는 것이다.
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}
props
객체에서 children
키에는 중첩된 자식 컴포넌트가 담겨있다.function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
props는 컴포넌트는 항상 정적인것이 아니며, 시간에 따라 바뀔 수 있다. 그러나 동시에 불변(immutable)이다. 따라서 props의 변경은 부모 컴퍼넌트에서 새 props 즉, 새 객체를 props에 할당하는 것으로만 가능하다. 새 props 객체로 교체되면 과거 props에 할당된 메모리가 수거되며 삭제된다.
Conditional Rendering
React에선 자바스크립트 문법인
if
문, &&
와 ? :
연산자 등을 사용해 JSX를 선택적으로 렌더링할 수 있다.컴포넌트는 반드시 무언갈 반환해야 한다. 하지만 경우에 따라 특정 조건에 컴포넌트가 스크린에 렌더되지 않아야 한다. 이 경우
null
을 반환하면 된다. 하자만 실무에서 이 방법은 잘 사용되지 않고, 보통 부모 컴포넌트의 JSX에 선택적으로 포함, 제외되는 방식을 사용한다.JSX 요소(
<li>
와 같은)는 실제 DOM 노드가 아니고 내부 상태도 보유하지 않는 일종의 설명서 또는 청사진으로 인스턴스가 아니다. {isPacked && '✔'}
단축 평가를 사용한 이 표현식의 의미는 isPacked가 참이면 체크마크를 렌더하고 그렇지않으면 아무것도 렌더하지 않는다의 의미다. 자바스크립트의&&
표현식은 왼쪽 조건이 참이면 오른쪽 값으로 평가되고 거짓이면 표현식 전체가 false
로 평가된다. React는 false
를 null
과 undefined
와 같이 아무것도 렌더하지 않는 ‘구멍’으로 처리한다.&&
연산에서 조건이 오는 왼쪽에 숫자가 와선 안된다. 숫자 0
이 왼쪽에 오면 거짓으로 평가해 최종적으로 false가 되는게 아니라 그냥 0
자체로 평가된다. 즉, messageCount && <p>New messages</p>
에서 messageCount가 숫자 0이면 예상대로 렌더를 안하는게 아니라 0 자체를 렌더해버린다. 따라서 숫자가 조건에 사용된다면 다음과 같이 비교 연산을 통해 정확히 boolean으로 평가되게끔 하자. messageCount > 0 && <p>New messages</p>
.단축 평가가 깔끔한 코드 작성에 방해가 된다면
if
문과 재할당을 할수 있는 let
변수를 활용하자. let
변수에 기본 값을 할당하고 조건에 따라 재할당을 하는 방식이다. 가독성이 높고 유지보수에 이점이 있는 방법이다.Rendering Lists
배열의
map()
호출 바로 안에있는 JSX 요소는 항상 key가 필요하다. key는React가 배열의 변경 사항을 정확히 추론하여 올바른 DOM 트리를 업데이트하게 돕는다.key는 그때 그때 봐가며 만들지 말고 데이터에 포함시켜 놓는 것이 좋다.
map으로 JSX 요소 배열을 생성할 때 하나의 배열 항목이 한 개 이상의 배열 요소로 변환되어야 한다면 앞서 말한대로 여러 요소를 하나로 묶는 부모 요소가 있어야 한다. 이때 Fragment 단축 리터럴인
<> ... </>
에는 key 를 설정할 수 없다. 따라서 div나 <Fragment>
를 사용해야 한다.const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
데이터가 데이터베이스에서 오는경우 key는 데이터베이스의 유니크한 키를 사용하면 된다. 노트 필기 앱의 노트처럼 로컬에서 생성되는 데이터는 항목이 생성될 때 같이 증가하는 카운터(
crypto.randomUUID()
또는 uuid
패키지)를 사용하자.같은 배열에 있는 항목들은 고유한 key를 가져야 한다. 다른 별개 배열과 키가 겹치는건 문제없다.
키는 절대 바뀌면 안된다. 렌더링하는 동안 키를 생성해서도 안된다.
key는 폴더안 파일들의 파일 이름과 같다. 이름이 없고 순서로만 기억 한다면 파일 하나가 지워지는 순간 두번째가 첫번째가 되고 세번째가 두번째가 되는 혼선이 발생한다. 즉, 배열안 컴포넌트가 삽입, 삭제 등으로 순서가 바뀌더라도 문제없이 React가 식별하게 하기 위험이다.
key를 명시하지 않으면 React는 암묵적으로 배열 인덱스를 key로 사용한다. 하지만 배열 인덱스는 변하지 않는 고유값이 아니며 재정렬 되거나 항목 추가, 삭제에 따라 바뀌는 말 그대로 항목의 바뀌는 순서에 맞게 할당되는 순서 번호다. 따라서 고유하면서 ‘바뀌지 않아야 하는’ key로 사용하면 안된다. 같은 이유로
key={Math.random()}
도 안됟다. 이경우 매번 새 DOM을 생성하게 되고 상태를 상실하게 된다.key는 컴포넌트 props 객체에 포함되지 않는다.
Keeping Components Pure
안전성을 위해 컴포넌트는 오로지 연산만을 수행하는 순수 함수로 만들어야 한다.
순수함수란 다음 나열되는 특성을 갖춘 함수를 말한다.
- 호출 이전부터 존재하고 있는 그 어떤 객체나 변수에 영향을 끼치지 않고 자신의 연산만을 수행함.
- 동일한 인풋이면 항상 동일한 아웃풋을 반환한다.
React는 이 순수 함수의 개념을 중심으로 설계되었기 때문에 React는 개발자가 작성한 모든 컴포넌트가 순수 함수라 가정한다. 따라서 컴포넌트는 동일한 인풋을 받으면 항상 동일한 JSX를 반환해야 한다.
일반적으로 컴포넌트가 어떤 특정 순서로 렌더될 것이라 기대해선 안된다. 각 컴포넌트는 서로 JSX를 계산하는데 어떤 영향을 주지 않고 독립적으로 스스로 수행되어야 하며 어떤 임의의 순서를 예상하고 컴포넌트간 영향을 주도록 설계해선 안된다.
React에는 렌더링 동안 읽어들일 수 있는 세 종류의 인풋
props
, state
, context
이 있고, 이 세 가지 인풋은 항상 읽기 전용으로 다뤄야 한다(바꾸려면 이전걸 아예 버리고 새걸 만들어 바꿔야한다).컴퍼넌트가 렌더링되는 동안 기존의 변수나 객체를 절대 수정해서는 안된다(즉, 계속 말하듯 ‘순수’함수여야 한다). 따라서 사용자 입력에 대응해 무언갈 바꾸려면 변수를 변경하는 것 대신
state
설정을 해야한다. React가 제공하는 Strict Mode는 이 규칙을 깨는 컴포넌트가 있는지 찾을 수 있게 각 컴포넌트 함수를 두번씩 호출한다. 순수 함수는 오직 자신의 연산만 수행하고 동일 인풋, 동일 아웃풋 이므로 순수 함수를 두번 호출하는 것은 함수의 리턴 값을 포함해 그 어떤것도 한번 호출에서 바뀌지 않는다. 반면, 아래와 같은 비순수 함수는 호출을 한 번씩 하는것과 두번 씩 하는것에 차이가 발생한다.let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
Strict Mode 활성화는 루트 컴포넌트를
<React.StrictMode>
로 감싸면된다. 컴포넌트는 순수 함수고 따라서 함수 스코프 밖의 변수 또는 함수 호출 이전에 생성된 객체를 변경해선 안된다. 하지만 렌더링때 생성한 객체와 변수는 변경해도 된다. 아래가 그 예시다.
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
위 코드에서 렌더링(컴포넌트 함수 호출)로 변경된 변수아 배열은
TeaGathering
컴포넌트 내부에서, 동일한 렌더링때 생성되었으므로 문제없다. TeaGathering
밖의 그 어떤 코드도 알수 없는 컴포넌트 내부에서만 발생한 변경으로 이걸 local mutation이라한다.스크린 업데이트, 데이터 변경, 애니메이션 시작과 같은 변화를 사이드 이펙트(side effect)라고 한다. 사이드 이펙트는 말 그대로 렌더링 과정이 아닌 “사이드”에서 일어난다. React에서 사이드 이펙트는 보통 이벤트 핸들러 내부에 둔다. 이벤트 핸들러란 버튼 클릭과 같은 액션을 수행하면 React가 실행하는 함수를 말한다. 이벤트 핸들러는 컴포넌트 내부에서 정의되었음에도, 렌더링 과정에는 실행되지 않는다. 따라서 이벤트 핸들러는 컴포넌트와 달리 순수하지 않아도 된다.
사이드 이펙트를 위한 알맞는 이벤트 핸들러를 찾을 수 없다면, 최후의 수단으로 컴포넌트에서
useEffect
를 호출하여 사이드 이펙트를 반환된 JSX에 붙일 수 있다. 이는 React에게 나중에 렌더링 후 사이드 이펙트가 허용될 때 실행하라고 알려준다. 그러나 이 접근 방식은 최후의 수단이어야 한다.가능하다면 렌더링만으로 로직을 표현해야 한다.
리액트가 컴포넌트의 순수성에 신경쓰는 이유는 아래와 같은 이점 떄문이다.
- 동일 인풋, 동일 결과 이므로 다른 환경에서도 컴포넌트를 실행할 수 있다.
- 인풋이 바뀌지 않은 컴포넌트의 렌더링을 건너뜀으로써 성능을 향상시킬 수 있다. 인풋이 바뀌지 않으면 기존의 렌더 결과와 동일할 것이 보장되기 떄문이다.
- 딥 컴포넌트 트리를 렌더링하는 과정에서 일부 데이터가 변경되면, 리액트는 구형이된 렌더링을 중단하고 렌더링을 재시작 한다. 이렇게 언제든 연산을 중단 하려면 독립적으로 자신의 연산만을 수행하면 되는 컴포넌트의 순수성이 필요하다.
Understanding Your UI as a Tree

React 렌더 트리는 오로지 리액트 컴포넌트로 구성된다. UI 프레임워크로서 React는 플랫폼에 구애받지 않는다. 따라서 HTML 태그나 UIView와 같은 플램폼의 UI 기초요소는 React를 구성하는 부분이 아니다. 따라서 React 렌더 트리에는 HTML tag 같은 플랫폼 UI 기초요소가 포함되어 있지 않다.

모듈 의존성 트리에는 컴포넌트가 아닌 모듈도 포함한다. 번들러가 어떤 모듈을 번들에 포함시켜야 하는지 결정하기 위해 모듈 의존성 트리를 사용한다.
앱이 커질 수록 보통 번들 사이즈도 커진다. 번들 사이즈가 커지면 UI가 그려지는 시간이 지체된다. 따라서 번들 사이즈를 가늠하고 이에 대한 적당한 기술을 적용을 위해 모듈 의존성 트리에 대한 이해가 도움이 된다.
Share article