컴포넌트를 여러 개 만들다 보면 곧 벽에 부딪힌다. Greeting이 늘 '안녕하세요'만 찍으면 재사용의 의미가 반쪽이다. 어떤 자리엔 '김코딩님', 어떤 자리엔 '이해커님'을 넣고 싶다. 이렇게 부모 컴포넌트가 자식 컴포넌트에 값을 건네주는 통로가 props(속성)다. 함수에 인자를 넘기는 것과 똑같은 일인데, 태그 모양이라 처음엔 낯설 뿐이다.
props는 컴포넌트에 넘기는 인자다. 컴포넌트를 쓸 때 HTML 속성처럼 값을 적어주면, 그 값들이 하나의 객체로 묶여 컴포넌트 함수의 첫 매개변수로 들어온다. 관례상 이 매개변수 이름을 props라 짓는다.
function Welcome(props) {
return <h1>{props.name}님 환영합니다</h1>;
}
이제 이 컴포넌트를 쓸 때 name을 넘겨준다.
<Welcome name="김코딩" />
그러면 props는 { name: "김코딩" }이 되고, 화면에는 '김코딩님 환영합니다'가 나타난다. <Welcome name="이해커" />라고 쓴 자리엔 '이해커님 환영합니다'가 뜬다. 같은 컴포넌트인데 넘긴 값에 따라 다른 화면이 나온다. 함수에 다른 인자를 주면 다른 결과가 나오는 것과 똑같다. 문자열은 따옴표로 넘기지만, 숫자나 변수처럼 자바스크립트 값을 넘길 땐 <Welcome name={userName} />나 <Badge count={3} />처럼 중괄호를 쓴다. 따옴표는 글자 그대로의 문자열, 중괄호는 자바스크립트 값이라는 구분이 여기서도 그대로 적용된다.
구조분해로 필요한 값만 꺼낸다. props.name, props.age처럼 매번 props.를 붙이면 지저분하다. 그래서 실무에선 매개변수 자리에서 바로 구조분해(destructuring, 객체에서 값을 골라 꺼내는 문법)로 풀어 쓴다.
function Welcome({ name }) {
return <h1>{name}님 환영합니다</h1>;
}
{ name }은 넘어온 props 객체에서 name 하나만 꺼내 변수로 쓰겠다는 뜻이다. 결과는 앞과 똑같지만 코드가 한결 깔끔하다. 값이 여러 개면 { name, age, color }처럼 쉼표로 나열해 한 번에 꺼낸다. 리액트 코드에서 이 형태를 워낙 자주 보게 되니 눈에 익혀두는 게 좋다.
props는 읽기 전용이다. 여기서 초보가 꼭 한 번은 데는 지점이 있다. 자식 컴포넌트는 넘어온 props를 절대 고쳐선 안 된다. props.name = "딴사람" 같은 짓을 하면 안 된다는 뜻이다. props는 부모가 빌려준 값이라, 자식은 읽어서 쓸 수만 있고 바꿀 권한이 없다. 나는 예전에 자식 안에서 넘어온 목록 배열을 직접 push로 건드렸다가, 화면은 그대로인데 데이터만 이상해지는 유령 같은 버그로 반나절을 날렸다. 값을 바꿔야 하면 props를 고칠 게 아니라, 그 값을 소유한 부모의 상태를 바꿔야 한다.
기본값을 정해 빈자리를 채운다. 부모가 어떤 props를 안 넘길 수도 있다. 그럴 때를 대비해 구조분해 자리에서 기본값을 정해두면, 값이 없을 때 그 기본값이 쓰인다.
function Badge({ text, color = "gray" }) {
return <span style={{ color }}>{text}</span>;
}
<Badge text="신규" color="red" />라고 쓰면 빨간 '신규'가 나타나고, <Badge text="신규" />처럼 색을 빼면 color가 기본값 gray가 되어 회색 '신규'가 나타난다. 넘겨도 되고 안 넘겨도 무너지지 않으니, 컴포넌트가 한결 튼튼해진다.
children으로 태그 사이 내용을 받는다. props 중에는 children이라는 특별한 게 있다. 컴포넌트를 열고 닫는 태그 사이에 넣은 내용이 자동으로 children이라는 props로 들어온다.
function Card({ children }) {
return <div className="card">{children}</div>;
}
이렇게 만들어 두고 <Card><p>안녕하세요</p></Card>라고 쓰면, 태그 사이의 <p>안녕하세요</p>가 children으로 들어가 card 클래스를 두른 상자 안에 '안녕하세요'가 나타난다. 겉모양은 고정하고 안에 들어갈 내용만 바꾸고 싶을 때 유용하다. 상자, 팝업, 버튼 틀처럼 껍데기를 재사용하는 컴포넌트가 대개 이 방식으로 만들어진다.
함수도 props로 넘겨 이벤트를 위로 올린다. props로 넘기는 게 문자열이나 숫자만은 아니다. 함수도 넘길 수 있고, 이게 자식에서 일어난 일을 부모에게 알리는 표준 방법이다. 자식은 값을 못 바꾸니, 대신 부모가 준 함수를 불러 '나 눌렸어요'라고 신호를 보낸다.
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
부모는 이 버튼에 눌렸을 때 실행할 함수를 onClick이라는 props로 넘긴다.
<Button label="삭제" onClick={() => setItems([])} />
화면엔 '삭제'라고 적힌 버튼이 나타나고, 이걸 누르면 부모가 넘긴 함수가 실행되어 setItems([])로 목록을 비운다. 자식 Button은 목록이 뭔지도 모른 채, 그저 눌렸을 때 받은 함수를 부를 뿐이다. 데이터는 부모에서 자식으로 props를 타고 내려가고, 사건은 자식에서 부모로 함수 호출을 타고 올라간다. 이 두 방향의 흐름이 리액트 앱이 굴러가는 기본 골격이다.
props는 위에서 아래로만 흐른다. 리액트의 데이터에는 방향이 있다. props는 언제나 부모에서 자식으로, 위에서 아래로만 흐른다. 자식이 부모의 값을 직접 끌어오거나, 옆에 나란히 있는 형제 컴포넌트에 값을 슬쩍 건네줄 방법은 없다. 이걸 단방향 데이터 흐름(one-way data flow)이라 부른다. 처음엔 불편해 보이지만, 값이 한 방향으로만 흐르니 화면이 이상할 때 '이 값이 어디서 내려왔나'를 위로 거슬러 올라가며 추적하기가 쉽다. 여러 갈래로 값이 오가던 명령형 코드에서 원인을 못 찾아 헤매던 것과는 딴판이다.
그래서 두 형제 컴포넌트가 같은 값을 나눠 써야 하면, 그 값을 둘의 공통 부모로 끌어올려 상태로 두고, 각 자식에겐 props로 내려보낸다. 이 방식을 상태 끌어올리기(lifting state up)라 하는데, props가 위에서 아래로만 흐른다는 규칙에서 자연스럽게 나오는 해법이다. 값을 소유한 곳은 부모 하나로 두고, 자식들은 받아 쓰기만 한다.
결국 props는 컴포넌트를 잇는 배선이다. props는 부모가 자식에게 값을 내려주는 통로이자, 읽기 전용이라는 규칙 위에서 데이터의 흐름을 한 방향으로 정돈해 준다. 값을 넘겨 화면을 다르게 그리고, 기본값으로 빈자리를 메우고, children으로 껍데기를 재사용하고, 함수를 넘겨 사건을 위로 올린다. 이 배선을 익히고 나면 흩어져 있던 컴포넌트들이 하나의 앱으로 이어지기 시작한다.