5 minute read

5장 - ref: DOM 에 이름 달기 - Typescript

React 프로젝트에서 ref 를 사용하는 이유

  • ref는 프로젝트 전역적으로 작동하지 않고, 컴포넌트 내부에서만 작동합니다.

  • 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법 추천 -> ref

  • id 프로퍼티는 유일해야 하기에, ref를 써서 중복 문제를 미리 예방합니다!


예제 컴포넌트 생성 - Typescript Version

ValidationSample.css

.success {
    background-color : lightgreen;
}
.failure {
    background-color : lightcoral;
}
  • 이 파일을 불러오는 컴포넌트 내부에서 className
    .success or .failure 이라면, 정해진 색깔로 배경을 변경합니다.


ValidationSample.tsx

(...)
import { Component } from 'react';
// 위에서 작성한 css 파일 가져옴
import './ValidationSample.css';

// state 타입 지정을 위한 인터페이스
interface StateIFace{
    password : string;
    clicked : boolean;
    validated : boolean;
}

class ValidationSample extends Component {
    state : StateIFace = {
        password : '',
        clicked : false,
        validated : false,
    }

    handleChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
        this.setState({
            password : e.target.value
        });
    }

    // 클릭했으므로 clicked : true 하고, 
    // password가 '0000'과 같은지 확인한다.
    handleButtonClick = () => {
        this.setState({
            clicked : true,
            validated : this.state.password === '0000'
        })
    }

    render() {
        return (
            <div>
                <input
                    type="password"
                    value={this.state.password}
                    onChange={this.handleChange}
                    className={ (this.state.clicked) ? 
                    (this.state.validated ? 'success' : 'failure'): ''
                    }
                />
                <button onClick={this.handleButtonClick}>검증하기</button>
            </div>
        )
    }
}
export default ValidationSample;
  • <input/>의 속성부터 살펴 보도록 하겠습니다!

  • className :
    this.state.clicked ? (this.state.validated ? 'success' : 'failure') : ''
    하나씩 떼서 보도록 하겠습니다.

    • this.state.clicked : state.clicked 의 값은 true or false 값을 가지게 됩니다(인터페이스).
    • this.state.validated : state.validated 의 값도 true or false의 값을 가집니다.
    • (true or false) ? (true 일 때 실행) : (false 일 때 실행) 형식을 가졌습니다.
  • 메서드 설명 :
    clickedfalse라면 className''가 됩니다. 하지만,
    clickedtrue 라면 (this.state.validated ? 'success' : 'failure')
    를 실행하게 됩니다.
    clicked : true 인 상황에서, validatedtrue 혹은 false 이냐에 따라
    className : success or failure 가 됩니다.

  • 바로 다음 예제에서 reactref 속성을 이용해 DOM을 접근 해 보겠습니다.


Reactref 속성을 이용하기

ValidationSample.tsx

import React, {createRef} from 'react';
import './ValidationSample.css';
import { Component } from "react";

class ValidationSample extends Component {
    // HTMLInputElement 인터페이스를 적용하여 
    // 접근하는 'DOM' 객체가 <input>라는 것을 typescript에게 알립니다.
    input = createRef<HTMLInputElement>();

    state = {
        password : '',
        clicked : false,
        validated : false
    }
    handleChanged = (e : React.ChangeEvent<HTMLInputElement>) => {
        this.setState({
            password : e.target.value
        })
    }
    handleButtonClicked = () => {
        this.setState({
            clicked : true,
            validated : this.state.password === '0000'
        })

        // input을 선언했지만, 해당 객체가 null일 가능성이 있어 "?"로 Typescript에 알려줍니다.
        this.input.current?.focus();
    }

    render() {
        return (
            <div>
                <input
                    type='password'
                    value={this.state.password}
                    onChange={this.handleChanged}
                    className={this.state.clicked ? (this.state.validated ? 'success' : 'failure') : ''}
                    ref={this.input}
                />
                <button onClick={this.handleButtonClicked}>검증하기</button>
            </div>
        );
    }
}
export default ValidationSample;

컴포넌트에 ref 달기 - Typescript

  • 컴포넌트에도 ref를 달 수 있습니다.

  • 상위 컴포넌트에서 하위 컴포넌트의 DOM을 사용해야 할 때 쓸 수 있습니다.

  • 컴포넌트에 ref를 다는 방법은 DOMref를 다는 것과 동일합니다.

  • 다만, Typescript에서는 추후 배울 리덕스를 이용하여
    굳이 DOM을 건드릴 상황을 만들지 않는 것이 좋지 않을까 생각합니다.

ScrollBox.tsx

import React, { CSSProperties } from 'react'
import { Component } from 'react'


class ScrollBox extends Component{
    // JSX 작성 시, 상위 <div> 에서 this.box 선언 시
    // 컴포넌트 내부에서 box 변수를 찾을 수 없어 따로 선언해 주어야 합니다.
    // null로 "this.box"를 초기화 해줍니다!
    box: HTMLDivElement | null = null;
    
    render() {
        // 1. 에서 설명
        const style : CSSProperties = {
            border : '1px solid black',
            height : '300px',
            width : '300px',
            overflow : 'auto',
            position : 'relative'
        };
        const innerStyle = {
            width : '100%',
            height : '650px',
            background : 'linear-gradient(white, black)'
        };
        return (
            <div
                style={style}
                ref={(ref) => {this.box=ref}}>
                <div style={innerStyle}/>
            </div>
        );
    }
}
export default ScrollBox;
  • JSX 문법 작성 시, (ref)에 타입이 설정되지 않은 이유는, ref={} 내부에서 (ref) 작성 시 자동으로
    HTMLDivElement | null로 인식하기 때문입니다.
    • const style : CSSProperties로 따로 타입을 설정한 이유는,
      position : 'relative' 속성 선언 시 오류가 뜨므로,
      CSSProperties를 선언해 확실하게 만들어 줍니다.

컴포넌트 내부 메서드를 ref를 통해 사용하기 - Typescript

  • DOM 노드가 가진 고유의 값이 있습니다

scrollTop : 세로 스크롤바 위치(0 ~ 350)
scrollHeight : 스크롤이 있는 박스 안의 div 높이(650)
clientHeight : 스크롤이 있는 박스의 높이(300)
.... : 등등의 많은 속성

  • 스크롤을 맨 아래로 내리기 위해 scrollHeight - clientHeight 합니다.

ScrollBox.tsx

import React, { createRef, CSSProperties } from "react";
import { Component } from "react";


class ScrollBox extends Component{
    // HTMLDivElement 인터페이스를 적용하여 
    // scrollToBottom 메서드에서 this.box를 참조할 수 있게 만듭니다.
    box = createRef<HTMLDivElement>();

    scrollToBottom = () => {
        // this.box.current 를 선언해야 직접적인 속성에 접근 할 수 있습니다.
        // 1. 에서 자세히 설명
        const {scrollHeight, clientHeight} = this.box.current as {scrollHeight : number, clientHeight : number};

        // 2. 에서 자세히 설명
        if(this.box.current) {
            this.box.current.scrollTop = scrollHeight - clientHeight
        };
    }

    render() {
        const style : CSSProperties = {
            border : '1px solid black',
            width : '300px',
            height : '300px',
            overflow : 'auto',
            position : 'relative'
        }
        const innerStyle = {
            width : '100%',
            height : '650px',
            background : 'linear-gradient(white, black)'
        }

        // ref={(ref) => this.box = ref}에서 변형
        // Typescript에서는 먹히지가 않습니다..
        return(
            <div
                style={style}
                ref={this.box}
            >
                <div style={innerStyle}/>
            </div>
        )
    }
}
export default ScrollBox;
    • this.box.current 선언 시, HTMLDivElement | null 이 따라옵니다.
    • .current사용 시, ref로 선언 된 컴포넌트가 현재 존재하는지 모르기 때문입니다.
    • 따라서, as {scrollHeight : number, clientHeight : number}하여
      currentnull이 아니라는 것을 명시합니다. - Typescript 이기 때문.


    • if(this.box.current)가 선언된 것도 같은 맥락입니다.
    • this.box.currentnull이라면, scrollTop을 설정하는 것도 의미 없는 것이 됩니다.
    • 이런 오류를 Typescript가 미리 체크하기 때문에, 에러를 없애주기 위해
      이처럼 선언 해 줍니다.

이번엔 상위 컴포넌트에서 ref를 이용하여 조작해 보겠습니다

App.tsx

import React from "react";
import { Component } from "react";
import ScrollBox from "./ScrollBox5_2";


class App extends Component{
    scrollBox : ScrollBox | null = null;

    render() : JSX.Element{
        return (
            <div>
                <ScrollBox ref = {(ref) => this.scrollBox = ref}/>
                <button onClick={() => this.scrollBox?.scrollToBottom()}>
                     밑으로
                </button>
            </div>
        );
    }
}
export default App;
  • <ScrollBox> 내부에서 ref 선언 시 HTMLDivElement | null가 아닌
    ScrollBox | null로 인식됩니다.

  • 이에 따라 refnull로 초기화 해줍니다.

  • button 내부의 onClick 메서드에 this.scrollBox?로서 ?가 들어간 이유는
    모든 메서드 및 JSX 문법이 동시에 실행된다고 가정되었을 떄, 당장 존재하지 않을 수도 있기 때문입니다.

  • 이는 Javascript, Typescript가 가지고 있는 비동기 특성 때문입니다.

  • ?를 넣어 이 변수가 null로서 존재 할 수도 있다는 것을 Typescript에 알려줍니다!


결과물

gradientDiv


정리

나중에 리덕스 써서 한꺼번에 조정합시다..

근데 리덕스를 Type 적용하려면 엄청난 양의 코드가 추가 될 예정입니다 ㅎ하ㅏ하하

Updated: