Lifting State Up

- 하위 컴포넌트의 State를 공통 상위 컴포넌트로 올림

 

 

Shared State

- 리액트로 개발시, 하나의 데이터를 여러대의 컴포넌트에서 표현해야 할 때가 있음.
- 이럴 경우 데이터를 각각 보관하는 게 아니라, 가장 가까운 공통된 부모 컴포넌트의 state를 공유해서 사용함
- State에 있는 데이터를 여러개의 하위 컴포넌트에서 공통적으로 사용하는 경우

 

이렇듯 하위 컴포넌트가 공통된 부모 컴포넌트의 state를 공유하여 사용하는 것

 

 

 

 

사용법 : 썹씨온도와 화씨온도 나타내기

하위 컴포넌트에서 State 공유하기

물의 끓음 여부를 알려주는 컴포넌트
부모 컴포넌트, State로 온도 값을 가지고 있고, 사용자가 온도값을 변경할 때마다 handleChange() 함수 호출됨

BoilingVerdict 컴포넌트에 celsius 라는 prop으로 전달됨

 

 

 

입력 컴포넌트 추출하기

온도를 입력받기 위한 TemperatureInput 컴포넌트

위의 Calculator 컴포넌트에서 온도를 입력 받는 부분을 추출하여 별도의 컴포넌트로 만들었다. 추가적으로 props로 단위를 나타내는 scale을 추가하여 온도의 단위를 썹시, 화씨로 나타내도록 함

 

 

 

변경한 Calculater 컴포넌트

하나는 섭씨, 하나는 화씨 온도를 입력받게 한다. 그러나 문제점은 사용자가 입력한 온도값이 TemperatureInput 컴포넌트의 State에 저장되기 때문에 썹씨온도와 화씨온도값을 따로 입력받으면 두개의 값이 다를 수 있다.

 

이를 해결하기 위해 값을 동기화 시켜줘야 한다.

 

 

온도 변환 함수 작성하기

섭씨를 화씨로, 화씨를 섭씨로 변환하는 함수
위의 변환함수를 호출하는 함수
온도값과 변환하는 함수를 파라미터로 넣어줌

 

 

 

Shared State 적용하기

먼저 TemperatureInput 컴포넌트에서 온도값을 가져오는 부분을 다음과 같이 수정한다. 이렇게 하면 온도값을 컴포넌트 State에서가 아닌 props에서 가져오게 된다.

 

 

handleChange() 함수 역시 다음과 같이 수정, 변경된 온도값이 상위 컴포넌트로 전달된다.

 

 

최종적으로 완성된 TemperatureInput 컴포넌트, State는 제거되었고, 오로지 상위 컴포넌트에서 전달받은 값만을 사용

 

 

 

Calculator 컴포넌트 변경하기

temperature와 scale에 온도값과 단위값을 각각 저장

TemperatureInput 컴포넌트 사용 부분에서는 각 단위로 변환된 온도값과 단위를 props로 넣어주었고, 값이 변경되었을 때 업데이트 하기 위한 함수를 onTemperatureChange에 넣어줌.  즉 섭씨온도가 변경되면 단위가 'c'로 변경되고 화씨온도가 변경되면 단위라 'f'로 변경된다.

 

 

 

최종적으로 완성된 구조

상위 컴포넌트 Calculator에서 온도값과 단위값을 각각 가지고 있으면, 두개의 하위 컴포넌트는 각각 섭씨와 화씨로 변환된 온도값과 단위, 그리고 온도를 업데이트하기 위한 onTemperatureChange() 함수를 props로 가지고 있다.

'React' 카테고리의 다른 글

[React] Composition 방법과 Inheritance  (0) 2024.08.20
[React] 실습 : 섭씨온도, 화씨온도 표시하기  (0) 2024.08.19
[React] Form과 Controlled Component  (0) 2024.08.18
[React] List 와 Key  (0) 2024.08.13
[React] Conditional Rendering  (0) 2024.08.12

1. 일반 페이징

@Data
public class SearchDto {
    private String by;              // 무슨 방식으로 검색할지?
    private String keyword;         // 검색 키워드는 무엇인지?
    private String userEmail;
    private int tournamentIndex;
    private String title;
    private int goodsIndex;

    private int countPerPage = 12;   // 한 페이지당 보여줄 게시글 개수
    private int requestPage;        // 니가 요청한 페이지 번호
    private int totalCount;         // 전체 게시글의 개수
    private int maxPage;            // 조회할 수 있는 최대 페이지
    private int minPage = 1;        // 조회할 수 있는 최소 페이지
    private int offset;             // 거를 게시글 개수

    private int naviSize = 5;       // 한 페이지에 표시할 페이지 번호 수
    private int totalPage;          // 전체 페이지 수
    private int beginPage;          // 시작 페이지 번호
    private int endPage;            // 끝 페이지 번호
    private boolean showPrev;       // 이전 표시 여부
    private boolean showNext;       // 다음 표시 여부

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;       // 전체 갯수
        maxPage = totalCount / countPerPage + (totalCount % countPerPage == 0 ? 0 : 1);     // 최대 페이지
        minPage = 1;    // 최소 페이지
        offset = countPerPage * (requestPage - 1);  // 거를 갯수

        totalPage = (int)(Math.ceil(totalCount/(double)countPerPage));  // 전체 페이지 수
        beginPage = ((requestPage -1)/naviSize) * naviSize + 1;     // 시작 페이지 번호
        endPage = Math.min(beginPage + naviSize -1, totalPage);     // 끝 페이지 번호
        showPrev = beginPage != 1;                                  // 이전 표시 여부
        showNext = endPage != totalPage;                            // 다음 표시 여부
    }
}

SearchDto 이다. setTotalCount() 함수로 페이징을 구현할 예정이다.

 

 

// 랭킹 코멘트 조회
    public TournamentCommentDto[] getComments(int index, SearchDto search) {
        search.setTournamentIndex(index);
        search.setTotalCount(this.tournamentMapper.selectTournamentCommentsCount(index));
        return this.tournamentMapper.selectTournamentComments(search);
    }
    // 랭킹 조회
    @RequestMapping(value = "/ranking", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getRanking(
            @RequestParam("index") int index,
            @RequestParam(value = "page",required = false, defaultValue = "1") int page,
            SearchDto search
    ) {
        search.setRequestPage(page);
        search.setCountPerPage(5);
        TournamentCommentDto[] comments = this.tournamentService.getComments(index, search);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("comments", comments);
        modelAndView.addObject("paging", search);
        modelAndView.setViewName("tournament/ranking");
        return modelAndView;
    }

다음과 같이 SearchDto 객체를 만든 후, 객체의 setTotalCount() 함수를 불러와서 전체 댓글 갯수를 전송 후, 계산된 페이징 정보들을 객체에 저장한다.

 

 

    TournamentCommentDto[] selectTournamentComments(SearchDto search);
    int selectTournamentCommentsCount(int tournamentIndex);
    <select id="selectTournamentComments" parameterType="dev.hcs.mytournament.dtos.SearchDto" resultType="dev.hcs.mytournament.dtos.TournamentCommentDto">
        SELECT `comment`.`index`              AS `index`,
               `comment`.`tournament_index`   AS `tournamentIndex`,
               `comment`.`user_email`         AS `userEmail`,
               `comment`.`content`            AS `content`,
               `comment`.`created_at`         AS `createdAt`,
               `comment`.`modified_at`        AS `modifiedAt`,
               `comment`.`is_reported`        AS `isReported`,
               `user`.`nickname`           AS `userNickname`
        FROM `worldcup`.`tournament_comment` AS `comment`
            LEFT JOIN `worldcup`.`users` AS `user` on `comment`.`user_email` = `user`.`email`
        WHERE `tournament_index` = #{tournamentIndex}
        ORDER By `comment`.`created_at` DESC
        LIMIT #{countPerPage} OFFSET #{offset}
    </select>

    <select id="selectTournamentCommentsCount" parameterType="int" resultType="int">
        SELECT COUNT(0)
        FROM `worldcup`.`tournament_comment` AS `comment`
                 LEFT JOIN `worldcup`.`users` AS `user` on `comment`.`user_email` = `user`.`email`
        WHERE `tournament_index` = #{tournamentIndex}
        ORDER By `comment`.`created_at` DESC
    </select>

쿼리이다. 전체 댓글 갯수를 구한 후, 그 구한 갯수로 페이징 정보를 넣고, 페이징 객체를 파라미터로 전달하여 해당 LIMIT에 속한 댓글들을 요청받으면 된다.

 

        <input class="currentPageHidden" th:value="${paging.getRequestPage()}" type="hidden">
        <section th:if="${paging.getTotalPage() > 1}" class="paging-container">
            <ul th:if="${paging.isShowPrev()}" class="front-back-ul">
                <li><a th:href="@{/tournament/ranking (index=${tournament.getIndex()}, page=${paging.getBeginPage() - 1})}"><i class="fa-solid fa-arrow-left"></i></a></li>
            </ul>
            <ul th:each="num : ${#numbers.sequence(paging.getBeginPage(), paging.getEndPage())}" class="paging-ul">
                <li><a th:class="${num}" th:href="@{/tournament/ranking (index=${tournament.getIndex()}, page=${num})}" th:text="${num}">1</a></li>
            </ul>
            <ul th:if="${paging.isShowNext()}" class="front-back-ul">
                <li><a th:href="@{/tournament/ranking (index=${tournament.getIndex()}, page=${paging.getEndPage() + 1})}"><i class="fa-solid fa-arrow-right"></i></a></li>
            </ul>
        </section>
    </main>
    <footer th:replace="layouts/footer ::  footer"></footer>
</body>
</html>
<script th:if="${paging.getTotalPage() > 1}">
    const currentPageHidden = document.querySelector('.currentPageHidden').value;
    const currentPageATag = document.getElementsByClassName(currentPageHidden);
    console.log(currentPageATag[0]);
    currentPageATag[0].style.backgroundColor='#e74c3c';
    currentPageATag[0].style.color='white';
</script>

HTML에서 다음과 같이 Thymeleaf를 통해 페이징을 구현하였다.

 

 

2. 검색 페이징

    // 홈 화면에 대회들 정렬(페이징과 검색)
    public TournamentEntity[] getTournaments(SearchDto search) {
        if (search.getKeyword() == null || search.getKeyword().length() > 50) {
            search.setKeyword(null);
        }
        if (search.getBy() == null) {
            search.setBy(null);
        }

        search.setTotalCount(this.tournamentMapper.getTournamentTotalCount(search));
        return this.tournamentMapper.selectTournaments(search);
    }
    @RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getIndex(
            @RequestParam(value = "by", required = false, defaultValue = "latest") String by,
            @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword,
            @RequestParam(value = "page", required = false, defaultValue = "1") int page,
            SearchDto search,
            HttpSession session
    ) {
        search.setBy(by);
        search.setKeyword(keyword);
        search.setRequestPage(page);
        TournamentEntity[] tournaments = tournamentService.getTournaments(search);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("tournaments", tournaments);
        modelAndView.addObject("paging", search);
        modelAndView.setViewName("home/index");
        return modelAndView;
    }

다음과 같이 by 와 keyword를 이용하여 search 객체에 집어넣는다.

 

 

    TournamentEntity[] selectTournaments(SearchDto search);
    int getTournamentTotalCount(SearchDto search);
    <select id="selectTournaments" parameterType="dev.hcs.mytournament.dtos.SearchDto" resultType="dev.hcs.mytournament.entities.TournamentEntity">
        SELECT `index`                      AS `index`,
               `user_email`                 AS `userEmail`,
               `thumbnail`                  AS `thumbnail`,
               `thumbnail_file_name`        AS `thumbnailFileName`,
               `thumbnail_content_type`     AS `thumbnailContentType`,
               `title`                      AS `title`,
               `content`                    AS `content`,
               `play_count`                 AS `playCount`,
               `created_at`                 AS `createdAt`,
               `modified_at`                AS `modifiedAt`,
               `is_recognized`               AS `isRecognized`
        FROM `worldcup`.`tournament`
        WHERE `is_recognized` is true
        <if test="keyword != null">
            AND REPLACE(`title`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
        </if>
        <if test="by != null and by.equals('latest')">
            ORDER BY `created_at` DESC
        </if>
        <if test="by != null and by.equals('popular')">
            ORDER BY `play_count` DESC
        </if>
        LIMIT #{countPerPage} OFFSET #{offset}
    </select>

    <select id="getTournamentTotalCount" parameterType="dev.hcs.mytournament.dtos.SearchDto" resultType="int">
        SELECT COUNT(0)
        FROM `worldcup`.`tournament`
        WHERE `is_recognized` is true
        <if test="keyword != null">
            AND REPLACE(`title`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
        </if>
        <if test="by != null and by.equals('latest')">
            ORDER BY `created_at` DESC
        </if>
        <if test="by != null and by.equals('popular')">
            ORDER BY `play_count` DESC
        </if>
    </select>

동적 sql문인 if문과 LIKE와 CONCAT을 이용하여 검색을 구현한다.

 

 

 

        <section class="search-container">
            <form id="search-form" method="get" action="/">
                <label class="orderLabel">
                    <select name="by" id="">
                        <option th:selected="${paging.getBy() == 'latest'}" value="latest">최신순</option>
                        <option th:selected="${paging.getBy() == 'popular'}" value="popular">인기순</option>
                    </select>
                </label>
                <label class="searchLabel">
                    <input th:value="${paging.getKeyword()}" name="keyword" minlength="1" maxlength="50" placeholder="월드컵 제목을 입력하세요. (1자 이상 50자 이하)" type="search">
                </label>
                <input value="검색" type="submit">
            </form>
        </section>
        
        <....>

        <input class="currentPageHidden" th:value="${paging.getRequestPage()}" type="hidden">
        <section th:if="${paging.getTotalPage() > 1}"  class="paging-container">
            <ul th:if="${paging.isShowPrev()}" class="front-back-ul">
                <li><a th:href="@{/ (page=${paging.getBeginPage() - 1}, by=${paging.getBy()}, keyword=${paging.getKeyword()})}"><i class="fa-solid fa-arrow-left"></i></a></li>
            </ul>
            <ul th:each="num : ${#numbers.sequence(paging.getBeginPage(), paging.getEndPage())}" class="paging-ul">
                <li><a th:class="${num}" th:href="@{/ (page=${num}, by=${paging.getBy()}, keyword=${paging.getKeyword()})}" th:text="${num}">1</a></li>
            </ul>
            <ul th:if="${paging.isShowNext()}" class="front-back-ul">
                <li><a th:href="@{/ (page=${paging.getEndPage() + 1}, by=${paging.getBy()}, keyword=${paging.getKeyword()})}"><i class="fa-solid fa-arrow-right"></i></a></li>
            </ul>
        </section>

해당 키워드로 검색하여 검색 결과를 페이징하게 한다.

Form

- 양식
- 사용자로부터 입력을 받기 위해 사용

HTML 형식의 Form



 

Controlled Component

- 값이 리액트의 통제를 받는 Input Form Element
- 사용자의 입력을 직접적으로 제어할 수 있음!

 

HTML에서의 Form과 Controlled Component의 차이
이런식으로 사용자가 값을 입력시, 입력한 값을 직접적으로 제어할 수 있음
사용자가 입력한 값을 대문자로 변경

 

 

 

textarea 태그

- 여러 줄에 걸쳐 긴 텍스트를 입력받기 위한 HTML 태그

 

 

Select 태그

- Drop-down 목록을 보여주기 위한 HTML 태그

각각 input, textarea, select 태그



 

File Input 태그

- 디바이스의 저장 장치로부터 하나 또는 여러개의 파일을 선택할 수 있게 해주는 HTML 태그
- 읽기 전용이므로 Uncontrolled Component, 리액트의 통제를 받지 않는다.


 

Multiple Inputs

- 하나의 컴포넌트에서 여러개의 입력을 다루려면 어떻게 해야 할까?
- 여러개의 state를 선언하여 각각의 입력에 대해 사용

각각 set을 달리하여 값을 집어넣으면 된다.



 

Input Null Value

- 앞에서 배운 것 처럼 제어 컴포넌트에 value prop을 정해진 값으로 넣으면 코드 수정하지 않은 한 입력값 변경 불가능
- value prop을 넣되 자유롭게 입력하게 만들고 싶으면 값에 undifined 혹은 null을 넣으면 된다.

자유롭게 입력하고 싶을 때, null을 값에 넣음

 

 

 

 

실습 : 사용자 정보 입력받기

사용자 이름 입력 받기

 

import React, { useState } from "react";

function SignUp(props) {
    const [name, setName] = useState("");

    const handleChangeName = (event) => {
        setName(event.target.value);
    };

    const handleSubmit = (event) => {
        alert(`이름: ${name}`);
        event.preventDefault();
    };

    return (
        <form onSubmit={handleSubmit}>
            <label>
                이름:
                <input type="text" value={name} onChange={handleChangeName} />
            </label>
            <button type="submit">제출</button>
        </form>
    );
}

export default SignUp;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import SignUp from './chapter_11/SignUp';

ReactDOM.render(
  <React.StrictMode>
  <SignUp />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

 

이름을 입력 받으면 알림창에 사용자의 이름이 뜨도록 한다.

 

 

 

성별 정보 추가

import React, { useState } from "react";

function SignUp(props) {
    const [name, setName] = useState("");
    const [gender, setGender] = useState("남자");

    const handleChangeName = (event) => {
        setName(event.target.value);
    };

    const handelChangeGender = (event) => {
        setGender(event.target.value);
    };

    const handleSubmit = (event) => {
        alert(`이름: ${name}, 성별: ${gender}`);
        event.preventDefault();
    };

    return (
        <form onSubmit={handleSubmit}>
            <label>
                이름:
                <input type="text" value={name} onChange={handleChangeName} />
            </label>
            <br />
            <label>
                성별:
                <select value={gender} onChange={handelChangeGender}>
                    <option value="남자">남자</option>
                    <option value="여자">여자</option>
                </select>
            </label>
            <button type="submit">제출</button>
        </form>
    );
}

export default SignUp;

select 태그를 통해 handleChangeGender 라는 이벤트 핸들러로 성별 정보를 변경하게끔 만들었다. gender 라는 state 가 추가되었다.

 

 

 

 

입력 결과 다음과 같이 알림창이 뜬다.

'React' 카테고리의 다른 글

[React] 실습 : 섭씨온도, 화씨온도 표시하기  (0) 2024.08.19
[React] Lifting State Up  (0) 2024.08.19
[React] List 와 Key  (0) 2024.08.13
[React] Conditional Rendering  (0) 2024.08.12
[React] Event  (0) 2024.08.08

+ Recent posts