React

React 양방향 useState / 단방향 REF 데이터 흐름 사용하기

startfront 2025. 4. 3. 17:05

1. React 리렌더링

React 리랜더링이 되는 상황

 

1) 컴포넌트의 state가 변경되었을 때
2) 컴포넌트가 상속받은 props가 변경되었을 때
3) 부모 컴포넌트가 리렌더링이 된 경우 자식 컴포넌트는 모두 리렌더링

 

2. 양방향 데이터 바인딩 구현

state  를양방향 가져오기

: 서버로 붙어 가져온 정보를 인풋으로 저장하고 ->

   변하된 것을  반영하기 위해 양방향 처리 해줘야한다.

  1. AJAX로 데이터 가져오기: 서버에서 데이터를 받아와 state에 저장.
  2. 양방향 바인딩: input 필드와 state를 연결하여 양방향 데이터 바인딩을 구현.
  3. onChange 이벤트 처리: 입력 값이 변경되면 state를 업데이트하여 실시간 반영.
  4. 폼 검증: 사용자가 올바른 값을 입력했는지 확인.

- state의 객체를 변경할 때는 deep copy 후 변경된 내용을 setState함수를 통해 변경

3. React에서 map() 함수 활용

- map()함수

: 반복되는 컴포넌트를 렌더링하기 위해서 자바스크립트 내장 함수인 map() 사용

  배열.map(callback(currentValue, index, array)
          ,[this])
  callback
    currentValue : 현재 처리하고 있는 요소
    index : 현재 처리하고 있는 요소의 index
    array : 원본 배열 
    this : callback함수에서 사용할 this

 

  key : 
컴포넌트의 배열을 랜더링했을 때 어떤 원소에 변동이 있었는지 알아내기 위한 속성
key는 컴포넌트를 식별할 유일값으로 사용해야 한다. 없다면 index를 사용하기 

    const name =["눈사람" , "얼음", "바람", "봄", "꽃망울"];
    const nameList =name.map((name, index) => <li key ={index}>{index}</li>);

    return <ul>{nameList}</ul>;

 

3-1. 이벤트 함수와 map을 이용하여 간단한 목록관리 작성  해보기

 json 데이터 map 을 이용해서 ui 나오게 작성 + state 이벤트로 양방향 까지(폼검증, 수정)

 

- 상태 관리: useState 훅을 사용하여 배열 names, 문자열 inputText, 그리고 숫자 nextId를 상태로 관리

- 이벤트 처리: 입력값을 처리하고, 버튼을 클릭하여 항목을 추가하거나 삭제하는 함수 작성

- 렌더링: map()을 사용하여 names 배열을 순회하면서 li 항목들을 만들어 화면에 표시

 

- 상태 초기화

: useState를 사용하여 상태를 초기화합니다. 세 가지 상태(names, inputText, nextId)를 관리

const [names, setNames] = useState([
  { id: 1, text: "눈사람" },
  { id: 2, text: "얼음" },
  { id: 3, text: "바람" },
  { id: 4, text: "봄" },
]);

const [inputText, setInputText] = useState(""); //사용자가 입력한 텍스트를 관리
const [nextId, setNextId] = useState(5); //다음에 추가할 항목의 id를 관리

 

 

- handleRemove 함수

: 항목이 클릭될 때마다 해당 항목의 id를 이용해 목록에서 삭제 하는 함수 작성

const handleRemove = (id) => {
  const newNameList = names.filter((name) => id !== name.id);
  setNames(newNameList);
};

 

 

- handleClick 함수

: "추가" 버튼을 클릭했을 때 새 이름을 목록에 추가

 

배열 concat():

기존의 배열을 직접 수정하지 않고 새로운 배열을 생성해서 반환!!
concat()은 원본 배열을 변경하지 않고 새 배열을 반환
const handleClick = () => {
  const newNames = names.concat({ id: nextId, text: inputText }); // 새로운 객체 추가
  setNames(newNames); // 상태 업데이트
  setNextId(nextId + 1); // nextId 증가
  setInputText(""); // 입력값 초기화
};

 

- handleChange 함수

: 텍스트 입력창에서 사용자가 입력하는 값을 상태로 관리

const handleChange = (e) => {
  setInputText(e.target.value);
};

 

- map()을 사용

: map()을 사용하여 names 배열의 각 항목을 HTML li 요소로 변환

//names.map() => names 배열을 순회하여 각 항목을 <li> 태그로 변환
const nameList = names.map((name) => (
  <li
    key={name.id}
    onClick={() => {
      handleRemove(name.id);
    }}
  >
    {name.text}
  </li>
));

 

- return

return (
  <div>
    <input type="text" value={inputText} onChange={handleChange} />
    <button onClick={handleClick}>추가</button>
    <ul>{nameList}</ul>
  </div>
);

 

=> 사용자가 텍스트 입력란에 값을 입력하고

      "추가" 버튼을 클릭하면 해당 값이 목록에 추가되며, 각 항목을 클릭하면 삭제

4. React에서 ref 사용법

1.REF

: react에서 DOM요소에 직접 접근할 수 있도록 연결하는 기능

   => 양방향으로 처리하지 않고 단방향으로 가죠오기

 

React에서 단방향 데이터 흐름이란

 

상태(state)가 상위 컴포넌트에서 하위 컴포넌트로 흐르는 방식
이 흐름은 props를 통해 이루어 진다

 

그러나 ref는 이 흐름과는 별개로 DOM 요소에 직접 접근할 수 있게 해주는 방법

 

- Ref를 사용해야 할 때 !!

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
  • 애니메이션을 직접적으로 실행시킬 때.
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.
 input 요소에 focus 주기
 스크롤 박스 조작하기
canvas 요소에 그림 그리기
클래스형 컴포넌트
형식 1]
<요소  ref={ ref =>{this.input=ref}}/>

형식 2]
속성명 =  React.createRef();
<요소  ref={this.속성명}/>

함수형
const 변수명 = useRef(null);
<요소  ref={변수명}/>

 

- 클래스형 컴포넌트로 ref 작성해보기

import React, { Component} from 'react'

export default class OneWayBind extends Component {
    // ref는 React 컴포넌트의 DOM 요소나 클래스 인스턴스를 직접 참조하는 방법입니다.
    // createRef()는 각 input 요소에 대한 참조를 생성합니다.
    inputId = React.createRef();  // 아이디 입력 필드에 대한 ref 생성
    inputPw = React.createRef();  // 비밀번호 입력 필드에 대한 ref 생성

    // 버튼 클릭 시 호출되는 함수
    handleClick = () => {
        // ref를 사용하여 DOM 요소의 값에 접근합니다.
        const id = this.inputId.current.value;  // inputId에 해당하는 값 가져오기
        const pw = this.inputPw.current.value;  // inputPw에 해당하는 값 가져오기

        // 콘솔에 id와 pw 값을 출력합니다.
        console.log("id", id, "pw", pw);

        // 입력 후 값 초기화 (input 요소의 value를 빈 문자열로 설정)
        this.inputId.current.value = "";
        this.inputPw.current.value = "";
    };

    render() {
        return (
            <div>
                {/* 
                    JSX에서 <label> 요소는 htmlFor 속성을 사용하여 연결해야 합니다. 
                    기존의 HTML에서는 'for' 속성을 사용하지만, JSX에서는 'htmlFor'를 사용합니다.
                */}
                <label htmlFor='id'>
                    아이디:
                    <input type="text" id="id" ref={this.inputId} />  {/* 아이디 입력란 */}
                </label>

                <label htmlFor='pw'>
                    비밀번호:
                    <input type="password" id="pw" ref={this.inputPw} />  {/* 비밀번호 입력란 */}
                </label>
                
                {/* 검증하기 버튼을 클릭하면 handleClick 함수가 호출됩니다. */}
                <button onClick={this.handleClick}>검증하기</button>
            </div>
        );
    }
}

 

- 함수형 컴포넌트로 ref 작성

: useRef 훅 사용

  =>  useRef는 함수형 컴포넌트에서 DOM 요소나 값을 참조하는 데 사용

import React, { useRef } from 'react';

const OneWayBind1 = () => {

    // useRef는 React에서 DOM 요소나 값을 참조하기 위해 사용하는 Hook입니다.
    // 여기서는 input 요소들을 참조하기 위해 사용합니다.
    const inputId = useRef(null);  // 아이디 입력 필드를 참조하는 ref
    const inputPw = useRef(null);  // 비밀번호 입력 필드를 참조하는 ref

    // handleClick 함수는 버튼 클릭 시 실행됩니다.
    const handleClick = () => {
        // inputId.current.value를 통해 input 요소의 값을 가져옵니다.
        const id = inputId.current.value;  // 아이디 값 가져오기
        const pw = inputPw.current.value;  // 비밀번호 값 가져오기
        
        console.log("id", id, "pw", pw);  // 콘솔에 id와 pw 출력
        
        // 값을 입력 후 초기화 (각각의 input 값 비우기)
        inputId.current.value = "";  // 아이디 입력란 비우기
        inputPw.current.value = "";  // 비밀번호 입력란 비우기
    }

    return (
        <div>
            {/* JSX에서 <label> 요소는 htmlFor 속성을 사용하여 input 요소와 연결해야 합니다. 
                'for' 대신 'htmlFor'를 사용해야 합니다. */}
            <label htmlFor='id'>
                아이디:
                {/* ref는 input 요소에 대한 참조를 설정합니다. */}
                <input type="text" id="id" ref={inputId} />
            </label>

            <label htmlFor='pw'>
                비밀번호:
                {/* ref를 사용하여 비밀번호 입력란에 대한 참조 설정 */}
                <input type="password" id="pw" ref={inputPw} />
            </label>
            
            {/* 버튼 클릭 시 handleClick 함수 호출 */}
            <button onClick={handleClick}>검증하기</button>
        </div>
    );
};

export default OneWayBind1;

 

=> useRef는 상태 변경 없이 DOM을 직접 조작할 수 있는 방법으로,

      주로 폼 값 처리나 애니메이션, 타이머 등에 사용

함수형 컴포넌트와 클래스형 컴포넌트의 차이!!!!

  1. this:
    • 클래스형 컴포넌트: this를 사용하여 상태(this.state)와 메서드(this.setState)에 접근해야 합니다.
    • 함수형 컴포넌트: useState, useRef 등을 사용하여 상태를 관리하고, this를 사용할 필요가 없습니다.
  2. render():
    • 클래스형 컴포넌트에서는 render() 메서드 내에서 JSX를 반환합니다.
    • 함수형 컴포넌트에서는 return을 바로 사용하여 JSX를 반환합니다.
  3. 구조 분해 할당 위치:
    • 클래스형 컴포넌트에서는 render() 메서드 내에서 this.state를 구조 분해 할당하여 상태를 사용해야 합니다.
    • 함수형 컴포넌트에서는 useState 훅을 통해 얻은 data 객체를 바로 구조 분해 할당하여 사용합니다.

결론

  • 클래스형 컴포넌트에서는 this를 사용하여 상태메서드에 접근해야 하기 때문에 render() 메서드 내에서 this.state를 사용해야 합니다.
  • 함수형 컴포넌트에서는 this가 없으며, useState로 상태를 관리하고, 함수 내에서 직접 변수처럼 상태를 참조합니다.
React에서 함수형 컴포넌트를 처음 도입할 때,
상태 관리를 위한 기능이 부족했기 때문에 클래스형 컴포넌트만 사용

=> 클래스형 컴포넌트는 React의 초기 설계에 맞춰 객체지향적인 방식으로 상태를 다루게 되어 this를 사용
=> 함수형 컴포넌트의 장점을 살리기 위해 Hooks API를 도입
=> React가 도입한 **useState**와 useEffect 훅을 통해 상태와 라이프사이클을 관리

 

- input 요소에 focus 주기

validationSample.css

.success {
  background-color: lightgreen;
}
.failure {
  background-color: lightcoral;
}

 

validationSample.js

1. 클래스 컴포넌트

import React, { Component } from "react";  // React 라이브러리와 Component 모듈을 임포트
import "./ValidationSample.css";  // CSS 파일을 임포트하여 스타일 적용

class ValidationSample extends Component {
    // ref를 사용하여 input 요소에 접근하기 위해 createRef()로 ref를 생성
    inputId = React.createRef();  // 아이디 입력 필드의 ref
    inputPw = React.createRef();  // 비밀번호 입력 필드의 ref

    // 컴포넌트의 상태를 정의 (id, password, clicked, validated)
    state = {
        id: "",           // id 입력 필드의 값
        password: "",     // password 입력 필드의 값
        clicked: false,   // '검증하기' 버튼이 클릭되었는지 여부
        validated: false, // 입력 값이 유효한지 여부
    };

    // 입력 값 검증 함수
    validation = () => {
        // 비밀번호가 비어 있거나 8자리 미만인 경우
        if (this.state.password === "" || this.state.password.length < 8) {
            alert('비밀번호를 8자리 이상 입력하세요');  // 경고 메시지
            this.inputPw.current.focus();  // 비밀번호 입력 필드에 focus
            return false;  // 유효하지 않음
        }

        // 아이디가 비어 있는 경우
        if (this.state.id === "") {
            alert("아이디를 입력하세요");  // 경고 메시지
            this.inputId.current.focus();  // 아이디 입력 필드에 focus
            return false;  // 유효하지 않음
        }

        return true;  // 모든 검증을 통과하면 true 반환
    };

    // 입력 값 변경을 처리하는 함수
    handleChange = (e) => {
        this.setState({
            [e.target.id]: e.target.value,  // 입력된 값을 state에 반영
        });
    };

    // 검증하기 버튼 클릭 시 호출되는 함수
    handleClick = () => {
        // 검증을 수행하고 결과에 따라 상태 업데이트
        this.setState({
            clicked: true,  // 버튼 클릭 상태를 true로 설정
            validated: this.validation(),  // validation 결과로 상태 업데이트
        });
    };

    render() {
        // 구조 분해 할당을 통해 상태 값을 가져옴
        const { id, password, clicked, validated } = this.state;

        return (
            <div>
                {/* JSX에서 label 태그의 'for' 속성은 'htmlFor'로 작성 */}
                <label htmlFor="id">
                    아이디 :
                    {/* 입력 필드에 클래스 동적으로 추가 */}
                    <input
                        type="text"
                        className={clicked ? (validated ? "success" : "failure") : ""}
                        id="id"
                        ref={this.inputId}  // ref를 통해 아이디 입력 필드에 접근
                        value={id}  // 상태의 id 값을 입력 필드에 반영
                        onChange={this.handleChange}  // 입력 값 변경 시 handleChange 호출
                    />
                </label>
                <label htmlFor="password">
                    비밀번호 :
                    <input
                        type="password"
                        className={clicked ? (validated ? "success" : "failure") : ""}
                        id="password"
                        ref={this.inputPw}  // ref를 통해 비밀번호 입력 필드에 접근
                        value={password}  // 상태의 password 값을 입력 필드에 반영
                        onChange={this.handleChange}  // 입력 값 변경 시 handleChange 호출
                    />
                </label>
                {/* 검증하기 버튼 클릭 시 handleClick 호출 */}
                <button onClick={this.handleClick}>검증하기</button>
            </div>
        );
    }
}

export default ValidationSample;  // 컴포넌트를 외부에서 사용할 수 있도록 export

 

2. 함수 컴포넌트

확인 포인트!!

- data 객체로 id, password, clicked, validated 상태를 관리

 const [data, setData] = useState({
        id: "",        // 아이디 입력 값
        password: "",  // 비밀번호 입력 값
        clicked: false, // '검증하기' 버튼 클릭 여부
        validated: false, // 입력 값이 유효한지 여부
        
         // 구조 분해 할당을 통해 상태 값 쉽게 사용
 const { id, password, clicked, validated } = data;

 

-  useState, useRef 등을 사용하여 상태를 관리하고, this를 사용하지 않음

-   return을 바로 사용하여 JSX를 반환

=> 클래스 컴포넌트의 경우  render() 메서드 내에서 JSX를 반환

=> render() 메서드 내에서 this.state를 구조 분해 할당하여 상태를 사용!!!!!!!

import React, { useRef, useState } from 'react';  // React, useRef, useState 임포트
import "./ValidationSample.css";  // CSS 파일을 임포트하여 스타일 적용

const ValidationSample1 = () => {
    // input 요소에 접근하기 위한 ref 생성
    const inputId = useRef(null);  // 아이디 입력 필드에 대한 ref
    const inputPw = useRef(null);  // 비밀번호 입력 필드에 대한 ref

    // 상태를 관리하기 위해 useState를 사용
    // data 객체로 id, password, clicked, validated 상태를 관리
    const [data, setData] = useState({
        id: "",        // 아이디 입력 값
        password: "",  // 비밀번호 입력 값
        clicked: false, // '검증하기' 버튼 클릭 여부
        validated: false, // 입력 값이 유효한지 여부
    });

    // 입력 값 검증 함수
    const validation = () => {
        // 비밀번호가 비어 있거나 8자 미만일 경우
        if (data.password === "" || data.password.length < 8) {
            alert('비밀번호를 8자리 이상 입력하세요');  // 경고 메시지
            inputPw.current.focus();  // 비밀번호 입력 필드에 focus
            return false;  // 유효하지 않음
        }

        // 아이디가 비어 있는 경우
        if (data.id === "") {
            alert("아이디를 입력하세요");  // 경고 메시지
            inputId.current.focus();  // 아이디 입력 필드에 focus
            return false;  // 유효하지 않음
        } else {
            return true;  // 검증 통과
        }
    };

    // 입력 값 변경 함수 (양방향 데이터 바인딩)
    const handleChange = (e) => {
        setData({
            ...data,  // 기존 상태를 유지하면서, 변경된 값만 갱신
            [e.target.id]: e.target.value,  // 입력된 값으로 상태 업데이트
        });
    };

    // 검증하기 버튼 클릭 시 호출되는 함수
    const handleClick = () => {
        // 버튼 클릭 시 검증을 수행하고, 결과를 상태에 반영
        setData({
            ...data,
            clicked: true,  // '검증하기' 버튼 클릭 상태
            validated: validation(),  // 검증 결과 저장
        });
    };

    // 구조 분해 할당을 통해 상태 값 쉽게 사용
    const { id, password, clicked, validated } = data;

    return (
        <div>
            {/* 아이디 입력 필드 */}
            <label htmlFor="id">
                아이디 :
                <input 
                    type="text"
                    className={clicked ? (validated ? "success" : "failure") : ""}  
                    // 검증 후 상태에 따라 클래스 변경
                    id="id"
                    ref={inputId}  // ref를 사용하여 input에 직접 접근
                    value={id}  // 상태 값으로 입력 필드의 값을 설정
                    onChange={handleChange}  // 입력 값 변경 시 handleChange 호출
                />
            </label>

            {/* 비밀번호 입력 필드 */}
            <label htmlFor="password">
                비밀번호 :
                <input 
                    type="password"
                    className={clicked ? (validated ? "success" : "failure") : ""}  
                    // 검증 후 상태에 따라 클래스 변경
                    id="password"
                    ref={inputPw}  // ref를 사용하여 input에 직접 접근
                    value={password}  // 상태 값으로 입력 필드의 값을 설정
                    onChange={handleChange}  // 입력 값 변경 시 handleChange 호출
                />
            </label>
            
            {/* 검증하기 버튼 */}
            <button onClick={handleClick}>검증하기</button>
        </div>
    );
};

export default ValidationSample1;

 

- 결과 확인

 

- 비밀번호가 8자리 이하 일때 알람

 

- 검증 되었을 때 포커스 및 색상 변화

 

- 스크롤 박스 조작하기

=> overflow 에 대한 이해!!!!

 

  • overflow:
    • overflow는 콘텐츠가 요소를 벗어날 때 어떻게 처리할지를 결정합니다. 예를 들어, 요소의 크기가 고정되어 있는데 그 안에 담을 수 있는 콘텐츠가 그보다 많으면 스크롤을 표시할 수 있도록 설정할 수 있습니다.
    • CSS 속성 overflow: auto는 콘텐츠가 요소보다 클 때 자동으로 스크롤을 추가하는 역할을 합니다.
  • clientHeight:
    • 요소의 보이는 영역의 높이를 의미합니다.
    • 예를 들어, div의 실제 높이는 300px일 수 있지만, 그 안의 콘텐츠가 너무 많아 스크롤이 생기면, 보이는 영역의 높이는 300px 그대로입니다.
    • clientHeight는 요소의 전체 높이에서 스크롤바보더(border) 를 제외한 실제 보이는 높이를 말합니다.
  • scrollHeight:
    • 요소 내부의 전체 콘텐츠의 크기입니다.
    • 콘텐츠가 div의 크기보다 클 때, 스크롤을 사용하여 나머지 콘텐츠를 볼 수 있게 됩니다.
    • scrollHeight는 콘텐츠의 전체 높이입니다. 스크롤이 필요한 양을 말합니다.
  • scrollTop:
    • 현재 요소의 스크롤 위치입니다.
    • scrollTop은 스크롤이 얼마나 내려갔는지를 의미하는데, 0이면 맨 위에 있다는 뜻입니다. 예를 들어, 스크롤을 맨 아래로 내리면 scrollTop 값은 scrollHeight - clientHeight 값이 됩니다.

- ref 부모 -> 자식 연결이 가능!!!!

app에 ref를 연결하여 활용할 수 있다.

 

app.js

import React, { useRef } from "react";
import ScrollBox from "./ScrollBox";  // ScrollBox 컴포넌트 가져오기

function App() {
  const scrollBoxRef = useRef(null);  // ref 생성

  return (
    <div className="App">
      <ScrollBox ref={scrollBoxRef} />  {/* ScrollBox 컴포넌트 렌더링 */}
      
      <button onClick={() => {
        scrollBoxRef.current.scrollToBottom();  // 버튼 클릭 시 스크롤 맨 아래로 이동
      }}>
        맨 아래로
      </button>
    </div>
  );
}

export default App;

 

scrollBox.js

import React, { Component } from 'react'

export default class ScrollBox extends Component {
  // 부모 컴포넌트에서 호출하는 scrollToBottom 함수
  scrollToBottom = () => {
    const { scrollHeight, clientHeight } = this.box; // 콘텐츠 높이와 뷰포트 높이 가져오기
    this.box.scrollTop = scrollHeight - clientHeight; // 맨 아래로 스크롤
  };

  render() {
    // 스크롤박스 스타일
    const style = {
      border: "1px solid black",
      height: "300px",
      width: "300px",
      overflow: "auto", // 콘텐츠가 많으면 스크롤바 표시
      position: "relative",
    };

    // 내부 콘텐츠 스타일
    const innerStyle = {
      width: "100%",
      height: "1000px", // 콘텐츠 길이 설정
      background: "linear-gradient(white, black)", // 그라디언트 배경
    };

    return (
      <div 
        style={style} 
        ref={(ref) => { this.box = ref; }} // ref로 DOM에 접근
      >
        <div style={innerStyle}></div> {/* 스크롤이 가능한 콘텐츠 */}
      </div>
    );
  }
}

 

-  결과물

 

- 맨 아래로 클릭 시