ReScript in Korean

컴포넌트와 Props

원문

컴포넌트는 독립적으로 재사용 가능한 부분으로 UI를 쪼갤 수 있게 해줍니다. 그리고 각 부분을 독립적으로 구성할 수 있습니다. 이 페이지에서는 컴포넌트의 개념을 설명합니다.

컴포넌트는 무엇이죠?

리액트 컴포넌트는 props 객체를 매개 변수(UI의 동적 부분에 대해 설명하는 데이터)로 받고 UI 요소를 설명하며 React.element 타입을 반환하는 함수입니다.

이 개념의 좋은 점은 인풋과 아웃풋에만 집중할 수 있다는 것입니다. 컴포넌트 함수는 데이터를 받고 UI를 렌더링하기 위해 리액트 프레임워크에서 관리하는 불투명(opaque)한 React.element를 반환합니다.

컴포넌트 인터페이스 구현 방법에 대한 자세한 내용이 궁금하다면 JSX를 넘어서 페이지를 참조하세요.

컴포넌트 예제

리스크립트 리액트 컴포넌트가 어떻게 생겼는지 첫 예제를 살펴보죠.

/* src/Greeting.res */
@react.component
let make = () => {
<div>
{React.string("Hello ReScripters!")}
</div>
}

중요 컴포넌트 함수 이름을 항상 make로 지정하고 @react.component 속성을 추가하는 것을 잊지 마세요.

우리는 어떤 props 도 받지 않는 make 함수를 포함한 Greeting.res 파일을 만들었습니다. 이 함수는 React.element 타입을 반환합니다. 또한 DOM에 렌더링될 때 <div> Hello ReScripters! </div> 이렇게 표현됩니다.

또한 우리가 만든 함수가 리액트 컴포넌트의 순수 자바스크립트 버전으로 직접 변환되어 출력됨을 알 수 있습니다. 자바스크립트에서 <div>React.createElement("div",...) 호출로 어떻게 변환되는지 확인하세요.

Props 정의하기

리액트에서, props는 주로 단일 props 객체입니다. 리스크립트에서 우리는 이름 있는 인자를 사용해 props 매개 변수를 정의합니다. 예를 들면 다음과 같습니다.

/* src/Article.res */
@react.component
let make = (~title: string, ~visitorCount: int, ~children: React.element) => {
let visitorCountMsg = "You are visitor number: " ++ Belt.Int.toString(visitorCount);
<div>
<div> {React.string(title)} </div>
<div> {React.string(vistorCountMsg)} </div>
children
</div>
}

옵셔널 Props

이름 있는 인자의 모든 기능을 사용해 옵셔널 Props를 정의할 수도 있습니다.

/* Greeting.res */
@react.component
let make = (~name: option<string>=?) => {
let greeting = switch name {
| Some(name) => "Hello " ++ name ++ "!"
| None => "Hello stranger!"
}
<div> {React.string(greeting)} </div>
}

참고 @react.component 속성은 make 함수에 마지막 매개 변수에 암시적으로 ()를 추가합니다. (우리가 직접 하지 않아도 되게끔).

JSX에서 특별한 문법으로 옵셔널 Props 를 적용할 수 있습니다.

let name = Some("Andrea")
<Greeting ?name />

key 와 ref라는 특별한 Props

key 또는 ref 로 props를 정의할 수 없습니다. 리액트는 이 props를 다르게 취급하며 컴파일러는 컴포넌트 함수에서 ~key 또는 ~ref 인수를 정의할 때마다 오류를 발생시킵니다.

keyref에 상응하는 내용은 배열과 키 and 리액트 Refs 포워딩 섹션에서 더 자세히 설명합니다

허용되지 않는 Props 이름 (ex. keyword)

type같은 Prop 이름은 (<input type="text" /> 처럼) 구문상 유효하지 않습니다. type은 리스크립트에서 예약된 단어입니다. <input type_="text" /> 처럼 _를 붙여 사용하세요.

aria-*같은 경우는 ariaLabel처럼 카멜 케이스를 이용합니다. DOM 컴포넌트를 위헤 리스크립트-리액트는 내부적으로 aria-label로 번역합니다.

data-*는 경우는 좀 까다롭습니다. 단어에 -글자가 포함되는 것을 리스크립트는 허용하지 않기 때문입니다. 만일 <div data-name="click me" /> 처럼 data-*를 사용하고싶다면, React.cloneElement or React.createDOMElementVariadic 섹션을 살펴보세요.

Children Props

리액트에서 props.children 속성은 부모 엘리먼트와 관계된 계층 구조를 표현하는 특별한 속성입니다.

let element = <div> child1 child2 </div>

기본적으로 위 표현식과 같이 자식 속성을 전달할 때마다 childrenReact.element 타입으로 처리됩니다.

module MyList = {
@react.component
let make = (~children: React.element) => {
<ul>
children
</ul>
}
}
<MyList>
<li> {React.string("Item 1")} </li>
<li> {React.string("Item 2")} </li>
</MyList>

흥미롭게도 단 하나의 엘리먼트를 보내든 여러 엘리먼트를 보내든, 리액트는 늘 단 하나의 React.element 타입으로 children을 만들어 보냅니다.

children 타입을 재정의하는것도 가능합니다. 여기 예시가 있습니다.

컴포넌트와 string 타입인 children props

module StringChildren = {
@react.component
let make = (~children: string) => {
<div>
{React.string(children)}
</div>
}
}
<StringChildren> "My Child" </StringChildren>
/* 이건 타입 에러를 발생시킵니다. */
<StringChildren/>

컴포넌트와 옵셔널 React.element Children

module OptionalChildren = {
@react.component
let make = (~children: option<React.element>=?) => {
<div>
{switch children {
| Some(element) => element
| None => React.string("No children provided")
}}
</div>
}
}
<div>
<OptionalChildren />
<OptionalChildren> <div /> </OptionalChildren>
</div>

어떠한 자식 컴포넌트도 받지 않는 컴포넌트

module NoChildren = {
@react.component
let make = () => {
<div>
{React.string("I don't accept any children params")}
</div>
}
}
/* 컴파일러는 에러를 발생시킵니다. */
<NoChildren> <div/> </NoChildren>

Children props는 계층구조를 모델링하기위해 악용되는(abused) 편입니다. 예를들면 <List> <ListHeader/> <Item/> </List> (이 구조에서 List는 오직 Item 또는 ListHeader 엘리먼트를 받고싶습니다), 그러나 이런 종류의 제약 조건은 모든 컴포넌트가 React.element이기 때문에 적용하기 어렵습니다. 따라서 모든 Children Props가 실제로 Item 또는 ListHeader 타입인지 확인하기 위해서 악명높은 런타임 타입 검사가 필요합니다.

이 경우 가장 좋은 접근은 Children 대신 Props를 사용하는 것입니다. 예를 들어 <List header="..." items=[{id: "...", text: "..."}]/> 처럼 Props를 사용하는 것이죠. 이 방법은 제약 조건을 입력하기 쉽고 컴포넌트 엘리먼트를 실제 사용하는 곳에서 컴포넌트 Props 제약 조건을 기억하고있지 않아도 됩니다.

children의 가장 좋은 사용 사례는 의미적 순서나 구현의 상세를 제외하고 React.element를 전달하는 것입니다!

Props 와 타입 추론

리스크립트 타입 시스템은 사용되는 prop 을 살피고 매우 적절하게 타입을 추론합니다.

간단한 경우, 범위가 넓은 사용이나 실험적인 경우에는 타입 어노테이션은 생략하는 것이 좋습니다. 다음처럼 말이죠

/* Button.res */
@react.component
let make = (~onClick, ~msg, ~children) => {
<div onClick>
{React.string(msg)}
children
</div>
}

위 예시에서 onClickReactEvent.Mouse.t => unit로 추론되고 msgstring 그리고 childrenReact.element로 추론됩니다. 타입 추론은 값을 더 작은(privately scoped) 함수로 전달할 때 특히 더 잘 동작합니다.

엄격한 타입 추론으로 많은 키보드 입력이 필요하지 않지만, 우리는 가시성을 높이고 타입 오류를 방지하기 위해 공공 API를 대하는 것처럼 props의 타입을 명시적으로 입력하는 것을 여전히 추천합니다.

JSX에서 컴포넌트 사용

모든 리스크립트 컴포넌트는 JSX에서 사용할 수 있습니다. 예를 들면 Greeting 컴포넌트를 우리의 App 컴포넌트와 같이 사용싶을 때는 다음처럼 합니다.

/* src/App.re */
@react.component
let make = () => {
<div>
<Greeting/>
</div>
}

참고 리액트 컴포넌트는 대문자로 시작합니다. 원시 DOM 엘리먼트는 div 또는 button 처럼 소문자로 시작합니다. JSX 스펙과 코드 변환에 대한 정보는 JSX language manual section 여기서 더 찾을 수 있습니다.

컴포넌트 직접 작성하기

JSX 사용되는 컴포넌트를 작성하기위해 @react.component 데코레이터를 꼭 사용할 필요는 없습니다. 대신에 우리는 makemakeProps 함수로 makeProps: 'a => propsmake: props => React.element 함수를 정의해 이걸 리액트 컴포넌트처럼 동작하게 할 수 있습니다.

이것은 @obj의 고유한 버전, 또는 다른 명명된 파라메터를 받고 단일 props 구조를 반환하는 다른 모든 함수에서 작동합니다. 예를 들면 다음과 같습니다.

module Link = {
type props = {"href": string, "children": React.element};
@obj external makeProps:(
~href: string,
~children: React.element,
unit) => props = ""
let make = (props: props) => {
<a href={props["href"]}>
{props["children"]}
</a>
}
}
<Link href="/docs"> {React.string("Docs")} </Link>

@react.component 데코레이터와 생성된 인터페이스의 더 자세한 내용은 JSX를 넘어서 페이지에서 찾을 수 있습니다..

서브모듈 컴포넌트

또한 리액트 컴포넌트를 하위 모듈로 표현할 수 있으므로 합성(Composite) 컴포넌트에 대해 여러 파일을 만들 필요없이 더 복잡한 UI를 빌드할 수 있습니다. (서브 모듈은 어쨌든 부모 컴포넌트에서만 사용됨)

/* src/Button.res */
module Label = {
@react.component
let make = (~title: string) => {
<div className="myLabel"> {React.string(title)} </div>
}
}
@react.component
let make = (~children) => {
<div>
<Label title="Getting Started" />
children
</div>
}

위에 정의 된 Button.res 파일에는 이제 정규화된 모듈인 (<Button.Label title="My Button"/>)을 작성했기 때문에 다른 컴포넌트에서도 사용할 수 있는 Label 컴포넌트가 포함됩니다.

module Label = Button.Label
let content = <Label title="Test"/>

컴포넌트 네이밍

컴포넌트는 실제로 한 쌍의 함수이기 때문에 JSX에서 사용할 모듈에 속해야 합니다. 각각의 컴포넌트를 식별하는 목적으로도 모듈을 사용하는 것이 좋습니다. @react.component는 우리가 작성한 모듈에 따라 자동으로 이름을 추가합니다.

/* File.res */
/* 개발 도구에서 `File`이라는 이름으로 보일 거에요 */
@react.component
let make = ...
/* 개발 도구에서 `File$component`이라는 이름으로 보일 거에요 */
@react.component
let component = ...
module Nested = {
/* 개발 도구에서 `File$Nested`라는 이름으로 보일 거에요 */
@react.component
let make = ...
};

고차 컴포넌트(HoC)에 대한 동적 이름이 필요하거나 고유 디스플레이 이름을 설정하려면 React.setDisplayName(make, "NameThatShouldBeInDevTools") 함수를 이용합니다.

팁과 트릭들

  • 하나의 컴포넌트 파일로 시작하고 컴포넌트가 커지면 서브모듈 컴포넌트를 활용해보세요. 정말 필요할 경우 여러 파일로 분할하는 것을 고려하세요.
  • 디렉토리 계층구조를 평평하게(Flat)하게 유지하세요. article/Header.res 같은 계층 구조보다 ArticleHeader.res처럼요. 파일 이름은 코드베이스 전체에서 고유하므로 파일 이름은 ArticleUserHeaderCard.res처럼 구체적으로 서술되는 경향이 있습니다. 파일 이름 내에서 구성 요소의 의미를 명확하게 표현하기 때문에 검색할 때 매우 쉽습니다. 또한 리팩토링에서 유리하기 때문에 파일이름이 길다고 반드시 나쁜 것은 아닙니다.