<< 학습 목표 >>

1. 파일을 서버로 보낼 수 있다.

2. 서버가 보낸 파일을 다운 받을 수 있다.


리엑트를 사용해서 파일을 업/다운로드 하는 방법은 기존의 JS의 방법과 동일함


<< 파일 업로드 >>

 

프로젝트 -> src -> chapter05 -> Component11.jsx 파일을 추가하고 아래 코드를 추가하자

실제로 파일이 업로드가 되도록 하려면 서버에서 복잡한 과정을 거쳐야하므로 파일 업로드가 가능한 서버가 있다고 상상하자

아래 컴포넌트는 파일 업로드가 가능한 상상의 서버로 요청을 보내는 것

import React, {useState} from 'react';
import axios from 'axios';

function Component11() {
    const submit = (e) => {
        e.preventDefault();
    
        let formData = new FormData();
        formData.append("서버가 파일을 받기 위한 파라미터 이름", document.frm.uploadFile.files[0]);
    
        const fetch = async() => {
          await axios.post("파일 업로드가 가능한 상상의 서버", formData).then(response => {
            alert("성공!");
          }).catch((error) => {
            alert("실패!");
          });
        }
    
        fetch();
    }

    return(
        <div style={{"margin": "50px"}}>
            <form name="frm" onSubmit={submit}>
                <label>업로드 할 파일 : <input type="file" /> </label>
              <div>
                  <button type="submit">서버로 요청 보내기</button>
              </div>
            </form>
        </div>
    );
}

export default Component11;

한가지 특징적인 점은 JS로 파일을 보낼 때는 여러 설정을 해줘야하지만 axios 로 파일을 보낼 때는 보낼 파일을 담기만 하면 됨(1)


<< 파일 다운로드 >>


프로젝트 -> src -> chapter05 -> Component12.jsx 파일 추가하고 아래 코드를 추가하자

파일 다운로드 역시 일반적인 방법임

import React from 'react';

function Component12() {
    const download = async() => {
      let filename = "zoom.txt";

      const url = "http://localhost:3000/fileDownload?filename="+filename;

      // 리엑트에서는 location을 사용할 때 반드시 window. 을 붙여줘야함
      window.location.href = url;
    }

    const autoDownload = async() => {
        // a 태그 생성 및 자동 실행(클릭)
        const down = document.createElement("a");
        down.setAttribute("href", url);
        down.setAttribute("download", filename);
        down.setAttribute("type", "application/json");
        down.style.display = "none";
        down.click();
    }

    return(
        <div style={{"margin": "50px"}}>
            <div>
              <h1>a 태그로 다운로드</h1>
              <a href="다운로드 받을 파일의 경로" download={"다운로드 받을 파일의 이름"} type="다운로드 받을 파일의 형식">다운로드</a>
            </div>

            <div>
              <h1>클릭 시 다운로드로 이동</h1>
              <button type="button" onClick={download}>다운로드</button>
            </div>

            <div>
              <h1>클릭 시 다운로드로 이동</h1>
              <button type="button" onClick={autoDownload}>다운로드</button>
            </div>
        </div>
    );
}

export default Component12;

 

여기서 특징적인건 JS에서는 location 객체를 사용할 때 바로 사용할 수 있었지만 리엑트는 반드시 window.location 으로 접근(1)해야함

버튼 클릭 시 파일 자동 다운로드(2)도 추가적으로 넣었음

728x90
LIST

<< 학습 목표 >>

1. 쿠키를 생성해 데이터를 저장할 수 있다.

2. 쿠키에 저장된 데이터를 꺼낼 수 있다.

3. 서버가 보낸 쿠키 데이터를 꺼낼 수 있다.


이번 Chapter 를 시작 하기 전 프로젝트 -> src 에 chapter05 폴더를 추가하자

 

리엑트에서 쿠키를 사용하려면 프로젝트에 쿠키 라이브러리를 설치해야함

터미널에서 프로젝트 경로로 이동 후 쿠키 라이브러리를 설치하자

(1). 프로젝트 경로로 이동

(2). 쿠키 라이브러리 설치 명령


우선 리엑트에서 쿠키를 생성하고 꺼내 보고 그 다음 서버에서 보낸 쿠키를 꺼내보자

 

<< 리엑트에서 쿠키 생성 >>

리엑트에서 쿠키를 사용하기 위해서는 다음의 import 문을 추가해야함

  import { useCookies } from 'react-cookie';

 

쿠키를 사용할 는 다음의 코드를 사용함

  const [cookies, setcookies] = useCookies([]);

 

쿠키를 생성할 때는 setter 를 사용해 쿠키를 생성함

  setcookies("쿠키명", "값");

 

이제 실습을 해보자

프로젝트 -> src -> chapter05 -> Component07.jsx 파일을 추가하고 아래 코드를 추가하자

import React, {useState} from 'react';
import { useCookies } from 'react-cookie';

function Component07() {
    const [name, setName] = useState();
    const [value, setValue] = useState();
    const [cookies, setCookies] = useCookies();

    function saveAtCookie() {
        setCookies(name, value);
    }

    return(
        <div style={{"margin": "50px"}}>
            <div style={{"margin": "20px"}}>
                <label htmlFor="name">쿠키 명 :&nbsp;&nbsp;</label>
                <input type="text" id="name" onChange={(e) => setName(e.target.value)}  />
            </div>

            <div style={{"margin": "20px"}}>
                <label htmlFor="value">쿠키에 저장할 데이터 :&nbsp;&nbsp;</label>
                <input type="text" onChange={(e) => setValue(e.target.value)} />
            </div>

            <div style={{"margin": "20px"}}>
                <button type="button" onClick={saveAtCookie}>쿠키에 저장</button>
            </div>
        </div>
    );
}

export default Component07;

<< 코드 설명 >>

 

(1). 쿠키를 사용하기 위한 import

(2). 쿠키를 사용하기 위해 useCookies 훅을 사용함

  지금은 쿠키를 생성하는 방법을 배우고 있기 때문에 setter만 사용함

(3). 이름이 name인 쿠키에 value를 저장함

 

이제 리엑트 프로젝트를 실행시키고 이 컴포넌트를 웹 페이지에 출력하자

쿠키 명과 저장할 데이터를 임의로 입력 후 [ 쿠키에 저장 ] 버튼을 눌러보자

화면에 보이는 변화는 없지만 내가 지정한 이름의 쿠키에 값이 저장되 있음

 

쿠키를 확인하기 위해서는 브라우저의 개발자 도구를 사용해야함

개발자 도구는 브라우저 내에서 [ F12 키 ] 를 누르거나 아래 순서를 따라가면 됨

 

개발자 도구 내에서는 엣지와 크롬 브라우저 모두 동일하므로 아래 설명을 따라가자

개발자 도구 내에서 쿠키를 확인하려면 [ 응용 프로그램 ] 또는 [ 애플리케이션 ] 또는 [ Application ] 탭으로 이동(1) -> [ 쿠키 ] (2) -> 쿠키 목록(3) 확인

 

지금 우리가 생성한 쿠키는 만료 시간이 없기 때문에 브라우저를 닫으면 사라지는 쿠키임

일정 기간 동안 쿠키를 유지하고 싶다면 다음과 같이 만료 시간을 지정해줘야함

만료 시간은 setter 의 세번째 인자로 넣어주며 "maxAge" 속성의 값으로 초단위의 만료 시간을 지정함

3600은 1시간을 초로 변환한 것

  - 1분 = 60초

  - 1시간 = 60분

  - 1시간은 1분이 60개 있는거니 60(1분의 초) * 60 => 3600


<< 쿠키에 저장된 값 가져오기 >>

 

이번에는 위에서 생성한 쿠키의 값을 가져오자

프로젝트 -> src -> chapter04 -> Component08.jsx 를 추가하고 아래 코드를 추가하자

import React, {useState} from 'react';
import { useCookies } from 'react-cookie';

function Component08() {
    const [name, setName] = useState();
    const [value, setValue] = useState();
    const [cookies, setCookies] = useCookies();

    function getAtCookie() {
        if(name in cookies) {
            setValue(cookies[name]);
        } else {
            setValue("쿠키 명이 잘못되었습니다.");
        }
    }

    return(
        <div style={{"margin": "50px"}}>
            <div style={{"margin": "20px"}}>
                <label htmlFor="name">쿠키 명 :&nbsp;&nbsp;</label>
                <input type="text" id="name" onChange={(e) => setName(e.target.value)}  />
            </div>

            <div style={{"margin": "20px"}}>
                <p>쿠키에 저장한 데이터 : {value}</p>
            </div>

            <div style={{"margin": "20px"}}>
                <button type="button" onClick={getAtCookie}>가져오기</button>
            </div>
        </div>
    );
}

export default Component08;

<< 코드 설명 >>

(1). 쿠키를 사용하기 때문에 useCookies 훅 사용

(2). [ 가져오기 ] 버튼 클릭 시 (3)의 함수 호출

(3). 화면에 입력한 쿠키 명이 쿠키 목록 안에 있는지 확인 후 있다면 해당 쿠키의 값을 value 변수에 저장함

  화면에 입력한 쿠키 명이 쿠키 목록 안에 없다면 value 변수에 "쿠키 명이 잘못되었습니다." 를 저장함


<< 컴포넌트가 마운트 되며 쿠키의 값 가져오기 >>

 

이 부분을 배우기 전 Component07 컴포넌트를 페이지에 출력하고 이름이 name1인 쿠키에 data1 을 저장하자

 

위에서는 컴포넌트가 마운트 된 후 쿠키의 값을 가져와봤음

여기서는 컴포넌트를 마운트 하면서 쿠키의 값을 가져와보자

 

프로젝트 -> src -> chapter05 -> Component09.jsx 파일을 추가 후 아래 코드를 추가하자

import React, {useState, useEffect} from 'react';
import { useCookies } from 'react-cookie';

function Component09() {
    const [value, setValue] = useState();
    const [cookies, setCookies] = useCookies(["name1"]);

    useEffect(() => {
        setValue(cookies["name1"]);
    }, []);

    return(
        <div style={{"margin": "50px"}}>
            <div style={{"margin": "20px"}}>
                <p>name1 쿠키에 저장한 데이터 : {value}</p>
            </div>
        </div>
    );
}

export default Component09;

<< 코드 설명 >>

 

(1). 지금까지 우리는 useCookies 훅을 사용할 때 인자로 아무것도 넣지 않았음

  useCookies 훅을 사용할 때 인자를 아무것도 넣지 않으면 모든 쿠키들을 불러와 cookies 변수에 저장함

  useCookies 훅을 사용할 때 문자열 배열을 인자로 넣으면 해당하는 이름의 쿠키들만 불러와 cookies 변수에 저장함

(2). useEffect 훅을 사용해 컴포넌트가 마운트될 때 불러온 name1 쿠키의 값을 value 변수에 저장함

 

여기서 이제 useCookies 훅을 사용할 때 두 가지 방법이 있다는걸 알았음

바로 위 코드 설명에서 언급한것처럼 useCookies 훅에 인자를 넣지 않으면 모든 쿠키들을 불러와 cookies 변수에 저장함

useCookies 훅에 인자를 넣으면 해당 하는 쿠키들만 불러와 cookies 변수에 저장함

 

사이트 내 쿠키가 너무 많거나 잘못된 쿠키에 접근하면 안된다 라면 useCookies 훅에 인자로 문자열 배열을 넣어서 필요한 쿠키만 불러와서 사용하면 됨

사이트 내 모든 쿠키를 불러와 찾아봐야한다면 useCookies 훅에 인자를 넣지 않으면 됨

 

 

이라고 다른 블로그나 ChatGPT가 그렇게 설명을 해주는데 인자를 넣어도 모든 쿠키를 불러와 저장하고 있음

 

위와 같이 Component09 에 console.log 를 사용해 불러온 쿠키를 출력해보면 모든 쿠키가 출력되는걸 알 수 있음

만약 생성한 쿠키가 없다면 Component07 컴포넌트를 추가하고 쿠키를 몇 개 추가하고 Component09 컴포넌트에서 확인해보자

쿠키의 값을 불러오는 부분을 배우고 있으므로 "왜 여러번 출력되지?" 보다 출력된 쿠키를 주목하자

우리는 분명 인자에 ["name1"] 을 넣어 이름이 name1인 쿠키만 불러왔는데 cookies 변수 안에는 모든 쿠키가 저장됬음

 

쿠키가 더 많아져야 하는건지, 어떤 변경 사항 때문인지는 잘 모르겠으나 인자를 넣어도 모든 쿠키를 불러오고 있음

 

그래도 특정 쿠키가 필요할 때는 문자열 배열을 인자로 넣자


<< 서버가 보낸 쿠키 확인하기 >>

 

-- 이번 내용을 배우기 전 서버에서 준비해야될 사항 --

더보기

노드 서버의 컨트롤러에 아래 코드 추가

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.get('/cookie/save', (req, res) => {
    res.cookie("someCookie", "someData");
    res.end();
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

(1). somCookie 라는 이름의 쿠키에 "someData" 데이터를 저장해 클라이언트에게 보냄


 

지금까지 쿠키를 생성할 때 리엑트(클라이언트)에서 직접 쿠키를 생성했는데 서버가 쿠키를 생성해 전달하고 리엑트에서는 서버가 전달한 쿠키를 꺼내야하는 경우도 생김

이번에는 쿠키를 생성해 전달하는 서버로 요청을 보내고 응답을 받았을 때 서버가 보낸 쿠키를 꺼내보자

 

프로젝트 -> src -> chapter05 -> Component10.jsx 파일을 추가하고 아래 코드를 추가하자

import React, {useState} from 'react';
import { useCookies } from 'react-cookie';
import axios from 'axios';

function Component10() {
    const [value, setValue] = useState();
    const [cookies, setCookies] = useCookies();

    const success = (response) => {
        setValue(cookies["someCookie"]);
    }

    const error = () => {
        console.log("문제가 발생했습니다.");
    }

    const sendRequest = () => {
        axios.get("http://localhost:3001/cookie/save", { withCredentials: true }).then(success).catch(error);
    }

    return(
        <div style={{"margin": "50px"}}>
            <div>
                <button type="button" onClick={sendRequest}>서버로 요청 보내기</button>
            </div>

            {
                value != null && (
                    <div style={{"margin": "20px"}}>
                        <p>서버가 이름이 someCookie인 쿠키에 저장한 데이터 : {value}</p>
                    </div>
                )
            }
        </div>
    );
}

export default Component10;

<< 코드 설명 >>

(1). axios 로 요청을 보낸 후 서버가 보낸 쿠키를 받으려면 반드시 withCredentials 옵션을 true 로 설정해 요청을 보내야함

  이 옵션이 빠지면 서버는 쿠키를 보내지만 클라이언트에서 쿠키를 저장하지 않음

728x90
LIST

<< 학습 목표 >>

1. GET 방식으로 파라미터 없이 요청을 보낼 수 있다.

2. GET 방식으로 파라미터를 담아 요청을 보낼 수 있다.

3. POST 방식으로 요청을 보낼 수 있다.

4. 응답 상태 코드에 따라 서로 다른 처리를 할 수 있다.

5. 응답 데이터를 받을 수 있다.


-- 이번 내용을 배우기 전 서버에서 준비해야될 사항 --

더보기

노드 서버를 사용하고 있다면 아래 과정을 따라오고 다른 서버를 사용하고 있다면 따라오지 않아도 됨

1. 노드 서버 프로젝트에 cors 라이브러리 설치

 

1-1. 터미널에서 노드 서버 프로젝트로 이동 : cd (노드 서버 프로젝트 명)

1-2. cors 라이브러리 설치 : npm install cors

 

 

2. 노드 서버의 컨트롤러에서 cors 라이브러리를 사용하도록 설정


 

리엑트가 서버로 요청을 보낼 때는 Axios 를 사용해 보낼 수 있음

 

Axios 를 사용하려면 우선 프로젝트에 Axios 를 설치해야함

  Axios 설치 명령 : npm install axios

 

Axios를 설치 할 때는 지금까지와 마찬가지로 터미널에서 명령을 입력해 설치함

터미널 창에서 프로젝트로 이동 후 Axios 를 설치하자


Axios 를 사용해 서버로 GET, POST 요청을 보내고 응답을 받을 수 있음

 

서버로 요청을 보낼 때는 제일 먼저 Axios 를 import  해야함

  Axios import 코드 : import axios from 'axios';

 

서버로 GET 요청을 보낼 때는 Axios 의 get 함수를 사용함

  axios.get("url").then(성공 응답을 받았을 때 처리할 함수).catch(실패 응답을 받았을 때 처리할 함수)

 

서버로 POST 요청을 보낼 때는 Axios 의 post 함수를 사용함

  axios.post("url").then(성공 응답을 받았을 때 처리할 함수).catch(실패 응답을 받았을 때 처리할 함수)

 

위 GET, POST 방식 요청 방법은 간단하게만 언급한 것으로 본격적으로 요청을 보내고 응답을 받는 방법을 배워보자


<< GET 방식으로 요청하고 다양한 상태 코드로 응답 받기 >>

 

-- 이번 내용을 배우기 전 서버에서 준비할 사항 --

더보기

노드 서버의 컨트롤러에 아래 코드 추가

( 노드 서버를 사용하고 있다면 아래 코드를 그대로 따라 입력하고 다른 서버를 사용하고 있다면 해당 서버의 언어에 맞게 바꿔 입력하기 )

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.get('/get/simple/200', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    console.log("상태 코드 200 으로 응답합니다.");
    res.status(200).send();
});

app.get('/get/simple/300', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    console.log("상태 코드 300 으로 응답합니다.");
    res.status(300).send();
});

app.get('/get/simple/400', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    console.log("상태 코드 400 으로 응답합니다.");
    res.status(400).send();
});

app.get('/get/simple/500', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    console.log("상태 코드 500 으로 응답합니다.");
    res.status(500).send();
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

 << 코드 설명 >>

 

(1). 클라이언트가 GET 방식으로

(2). http://localhost/get/simple/200 경로로 요청을 보낸다면

(3). 상태 코드 200으로 응답하겠다.

 

이제 노드 서버를 실행시키자

노드 서버를 실행시키는 명령 : node (컨트롤러 파일명)


 

먼저 가장 간단한 형태인 데이터 없이 GET 방식으로 서버에 요청을 보내보자

요청 경로에 따라서 서버는 200, 300, 400, 500 상태 코드로 응답함

 

프로젝트 -> src -> chapter04 -> Component01.jsx 파일을 추가하고 아래 코드를 추가하자

import React from 'react';
import axios from 'axios';

function Component01() {
    function success() {
        console.log("응답을 받는데 성공했습니다.");
    }

    function error() {
        console.log("응답을 받았지만 문제가 생겼습니다.");
    }

    let sendRequest1 = () => {
        axios.get("http://localhost:3001/get/simple/200").then(success).catch(error);
    };

    let sendRequest2 = () => {
        axios.get("http://localhost:3001/get/simple/300").then(success).catch(error);
    };

    let sendRequest3 = () => {
        axios.get("http://localhost:3001/get/simple/400").then(success).catch(error);
    };

    let sendRequest4 = () => {
        axios.get("http://localhost:3001/get/simple/500").then(success).catch(error);
    };

    return(
        <div>
            <p>axios를 사용해 파라미터 없이 요청 보내기</p>
            <div><button type="button" onClick={sendRequest1}>요청 보내고 상태 코드 200 으로 응답 받는 상황</button></div>
            <div><button type="button" onClick={sendRequest2}>요청 보내고 상태 코드 300 으로 응답 받는 상황</button></div>
            <div><button type="button" onClick={sendRequest3}>요청 보내고 상태 코드 400 으로 응답 받는 상황</button></div>
            <div><button type="button" onClick={sendRequest4}>요청 보내고 상태 코드 500 으로 응답 받는 상황</button></div>
        </div>
    );
}

export default Component01;

<< 코드 설명 >>

(1). axios 를 사용하기 위해 import

(2). axios 로 요청을 보내고 200번대 상태 코드로 응답을 받았을 때 동작할 함수

(3). axios 로 요청을 보내고 300, 400, 500번대 상태 코드로 응답을 받았을 때 동작할 함수

(4). axios 로 요청을 보내는 함수

(5). 버튼을 클릭했을 때 적절한 함수를 호출 해 요청을 보냄

 

이제 리엑트 프로젝트를 실행시키고 이 컴포넌트를 출력해 각 버튼을 눌러보자

 

당연히 눈에 보이는 결과는 없음

왜? 위 코드 설명의 (2), (3) 을 다시 봐보면 응답을 받았을 때 console.log 로 출력하라고 되어있으므로...

 

브라우저 내 개발자 도구를 열고 [ console 패널 ] 로 가보자

서버가 상태 코드 200으로 응답했을 때만 success 함수가 호출되고 나머지 경우는 모두 error 함수가 호출된걸 알 수 있음

 

300, 400, 500 상태 코드는 특별한 경우이고 내가 서버에 대한 지식이 있거나 서버 개발자와 소통 해야되는 부분이므로 이제 언급하지 않겠음


<< GET 방식으로 요청하고 다양한 데이터를 응답 받기 >>

 

-- 이번 내용을 배우기 전 서버에서 준비할 사항 --

더보기

노드 서버의 컨트롤러에 아래 코드 추가

( 노드 서버를 사용하고 있다면 아래 코드를 그대로 따라 입력하고 다른 서버를 사용하고 있다면 해당 서버의 언어에 맞게 바꿔 입력하기 )

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.get('/get/data/simple', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    res.send("123");
});

app.get('/get/data/obj', (req, res) => {
    console.log("클라이언트의 요청을 받았습니다.");
    const obj = {
        name: "홍길동",
        age: 23
    };

    res.send(obj);
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

 

(1). 클라이언트에게 단순한 데이터를 보냄

(2). 클라이언트에게 객체를 보냄

 

노드 서버를 재시작 하자


 

아까와 마찬가지로 GET 방식으로 서버에 요청을 보내보자

이번에는 요청 경로에 따라서 서버는 데이터, 객체를 보냄

 

프로젝트 -> src -> chapter04 -> Component02.jsx 파일을 추가하고 아래 코드를 추가하자

import React from 'react';
import axios from 'axios';

function Component02() {
    function success(response) {
        console.log("응답을 받는데 성공했습니다.");
        console.log(response);
        console.log(response.data);
    }

    function error() {
        console.log("응답을 받았지만 문제가 생겼습니다.");
    }

    const sendRequest1 = () => {
        axios.get("http://localhost:3001/get/data/simple").then(success).catch(error);
    }

    const sendRequest2 = () => {
        axios.get("http://localhost:3001/get/data/obj").then(success).catch(error);
    }

    return(
        <div>
            <p>axios를 사용해 파라미터 없이 요청 보내기</p>
            <div><button type="button" onClick={sendRequest1}>요청 보내고 데이터를 응답 받은 상황</button></div>
            <div><button type="button" onClick={sendRequest2}>요청 보내고 객체를 응답 받은 상황</button></div>
        </div>
    );
}

export default Component02;

<< 코드 설명 >>

 

(1). 서버가 보낸 데이터를 받기 위해 매개변수 를 추가했음

(2). 서버가 보낸 응답 정보를 출력함

(3). 서버가 보낸 응답 정보 안에서 데이터에 접근하고 있음

 

이 컴포넌트를 출력한 후 버튼들을 눌러보자

그 후 개발자 도구 내 [ console 패널 ] 을 확인해보면 서버의 응답 정보와 서버가 보낸 데이터가 출력되는걸 볼 수 있음


<< GET 방식으로 요청할 때 데이터를 담아 요청하기 >>

 

-- 이번 내용을 배우기 전 서버에서 준비할 사항 --

더보기

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.get('/get/parameter', (req, res) => {
    console.log("파라미터 있는 요청을 받았습니다.");

    console.log("<< 클라이언트가 보낸 파라미터 >>");
    console.log("name => " + req.query.name);
    console.log("age => " + req.query.age);
    console.log("tel => " + req.query.tel);
    
    res.end();
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

 

(1). 클라이언트가 보낸 데이터는 req ( Request, 요청, 의 줄임말 ) 의 query 에 들어있음

  query.(파라미터명) 으로 클라이언트가 보낸 데이터에 접근할 수 있음

 

노드 서버를 재시작 하자


 

이번에는 요청을 할 때 데이터를 담아서 요청을 보내보자

프로젝트 -> src -> chapter04 -> Component03.jsx 파일을 추가하고 아래 코드를 추가하자

import React from 'react';
import axios from 'axios';

function Component03() {
    function success(response) {
        console.log("응답을 받는데 성공했습니다.");
    }

    function error() {
        console.log("응답을 받았지만 문제가 생겼습니다.");
    }

    const sendRequest1 = () => {
        let data = {
            name: "홍길동",
            age: 23,
            tel: "010-1111-1111"
        };

        let opt = {
            params: data
        }

        axios.get("http://localhost:3001/get/parameter", opt).then(success).catch(error);
    }

    return(
        <div>
            <p>axios를 사용해 파라미터와 함께 요청 보내기</p>
            <div><button type="button" onClick={sendRequest1}>파라미터(데이터)를 담아 요청을 보내는 상황</button></div>
        </div>
    );
}

export default Component03;

<< 코드 설명 >>

 

(1). 서버로 보낼 데이터

(2). 서버로 데이터를 보내기 위해 params 에 데이터를 담은 객체 생성

(3). 서버로 데이터를 보내기 위해 get 함수의 두 번째 인자로 opt 를 넣음

 

이 컴포넌트를 출력하고 버튼을 눌러보자

그 뒤 결과는 vs code 내 터미널에서 확인해야함

 

여기까지 GET 방식 요청을 통해

1. 서버로부터 상태 코드를 응답 받았을 때

2. 서버로부터 데이터를 응답 받았을 때

3. 서버에게 데이터를 보내는 방법

을 배웠음


이번에는 POST 방식 요청으로 서버에게 데이터를 보내는 방법을 알아보자

참고로 POST 방식 요청은 상태 코드를 응답 받았을 때, 데이터를 응답 받았을 때 처리는 GET 방식 요청과 완전히 동일함

 

<< POST 방식으로 요청할 때 데이터를 담아 요청하기 >>

 

-- 이번 내용을 배우기 전 서버에서 준비할 사항 --

더보기

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3001;

app.use(cors());

app.post('/post/parameter',  (req, res) => {
    console.log("파라미터 있는 요청을 받았습니다.");
    console.log(new Date().toLocaleString());

    console.log("<< 클라이언트가 보낸 파라미터 >>");
    console.log("name => " + req.query.name);
    console.log("age => " + req.query.age);
    console.log("tel => " + req.query.tel);

    res.end();
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

 

(1). 클라이언트가 POST 방식으로 http://localhost/post/parameter 경로로 요청을 보냈을 때 처리하기 위한 코드


 

POST 방식으로 요청할 때 데이터를 담아 요청할 컴포넌트를 추가하자

프로젝트 -> src -> chapter04 -> Component04.jsx 파일을 추가하고 아래 코드를 추가하자

import React from 'react';
import axios from 'axios';

function Component04() {
    function success(response) {
        console.log("응답을 받는데 성공했습니다.");
    }

    function error() {
        console.log("응답을 받았지만 문제가 생겼습니다.");
    }

    const sendRequest = () => {
        let data = {
            name: "홍길동",
            age: 23,
            tel: "010-1111-1111"
        };

        let opt = {
            params: data
        }

        axios.post("http://localhost:3001/post/parameter", null, opt).then(success).catch(error);
    }

    return(
        <div>
            <p>axios를 사용해 파라미터와 함께 요청 보내기</p>
            <div><button type="button" onClick={sendRequest}>파라미터(데이터)를 담아 요청을 보내는 상황</button></div>
        </div>
    );
}

export default Component04;

<< 코드 설명 >>

 

(1). 두 번째 인자 자리는 비워두고 세 번째 인자 자리에 보낼 데이터를 담아서 보낼 수 있음

728x90
LIST

<< 학습 목표 >>

1. 노드 제이에스 서버를 설치할 수 있다.

2. 노드 제이에스 서버를 실행시킬 수 있다.

3. 노드 제이에스 서버에 컨트롤러를 만들 수 있다.

4. 노드 제이에스 서버가 클라이언트에게 쿠키를 보낼 수 있다.

5. 노드 제이에스 서버가 세션에 데이터를 저장할 수 있다.


이번 Chapter에서는 Cookie, Session, Axios, File Upload / Download 를 배울 것임

이러한 내용들을 배우려면 서버가 필요함

가장 빠르게 배워 간단하게 사용할 수 있는 서버가 node.js ( 이하 노드 제이에스 라 칭함 ) 이므로 이번 Chapter 를 시작하기 전 노드 제이에스를 간략하게 배우자

 

단, 여기서는 Cookie, Session, Axios 를 배우기 위해 노드 제이에스의 최소한만 배우는 것이니 노드 제이에스 서버와 관련된 지식을 쌓을 순 없음

또한 여기서 사용하고 있는 용어가 이해되지 않는다면 그냥 따라오기만 해도됨

꼭 이해하고 싶다면 별도로 JSP/Servlet 관련 서적을 보고 우선 서버를 공부하고 오자

 

노드 제이에스 외에 Servlet, Spring, Spring Boot 등으로 서버를 구축할 수 있다면 이번 글은 넘어가도 됨


노드 제이에스로 서버를 구축하기 위해 프로젝트를 만들자

우선은 빈 프로젝트를 만들고 그 안에 노드 제이에스를 설치하는 과정임

 

<< 빈 프로젝트 생성 >>

(1). vs code -> [ 탐색기 ] 내 빈 영역 우클릭 -> [ 새 폴더 ]

(2). 폴더 이름은 임의로 지정 / 여기서는 node_server 로 했음

 

<< 노드 제이에스 설치 >>

방금 생성한 빈 프로젝트에 노드 제이에스를 설치하자

(1). vs code 터미널 창에서 빈 프로젝트로 이동

 빈 프로젝트로 이동하기 위한 명령 : cd (프로젝트명)

(2). 노드 제이에스 서버 설치

  노드 제이에스 서버 설치 명령 : npm install express

 

 

이제 빈 프로젝트에 클라이언트의 요청을 받을 수 있도록 컨트롤러를 추가하자

 

<< 파일 추가 >>

(1). 노드 제이에스 서버를 설치한 프로젝트 명 우클릭 -> [ 새 파일 ]

(2). 파일 이름을 임의로 짓고 파일의 확장자는 반드시 js 어야함 / 여기선 app.js 로 만듬

 

<< 컨트롤러 코드 추가 >>

방금 추가한 파일에 아래 코드 추가

const express = require('express');
const app = express();
const port = 3001;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

(1). 서버의 포트는 3001 번으로 지정

(2). 클라이언트가 GET 방식으로 http://localhost:3001/ 에 접근했을 경우 'Hello World!' 를 응답

 

여기까지 클라이언트의 요청을 받을 수 있는 준비가 다 됐음

 

 

이제 서버를 실행시키자

서버를 실행 시킬 때는 터미널 창에서 node (컨트롤러파일명) 명령을 입력하면 됨(1)

 

이제 브라우저를 열어 주소창에 http://localhost:3001/ 을 입력해 접근해보자

화면에 Hello World! 가 출력됐다면 노드 제이에스 서버가 제대로 실행된 것이고 그렇지 않다면 다시 이 글을 따라 올 것

이 글대로 따라오는데 되지 않는다면 따로 인터넷에 찾아보길


<< 클라이언트가 보낸 데이터 꺼내기 >>

클라이언트가 서버의 / 경로로 GET 방식으로 요청을 보낸다고 상상해보자

이때 클라이언트가 name=홍길동, age=20, tel=010-1111-1111 파라미터를 함께 보내는 상황임

클라이언트가 보낸 데이터를 꺼낼 때는 다음과 같음

 

아래 코드를 컨트롤러 파일에 덮어 씌우자

const express = require('express');
const app = express();
const port = 3001;

app.get('/', (req, res) => {
  const name = req.query.name;
  const age = req.query.age;
  const tel = req.query.tel;

  console.log("<< 클라이언트가 보낸 데이터 >>");
  console.log("name => " + name);
  console.log("age => " + age);
  console.log("tel => " + tel);

  res.sendStatus(200);
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

(1). 클라이언트가 보낸 데이터는 req.query.(name) 으로 꺼낼 수 있음

 

컨트롤러 코드가 변경 됐으니 노드 서버를 종료 시키고 다시 실행시키자

노드 서버를 종료 시킬 때는 터미널 창을 한번 클릭 한 후 Ctrl + C 를 누르면 됨

그 후 다시 노드 서버를 실행시키는 명령 ( node (컨트롤러 파일명) ) 을 입력하면 됨

 

브라우저의 주소창에 localhost:3001/?name=홍길동&age=20&tel=010-1111-1111 다음과 같이 입력해 클라이언트가 보낸 값이 제대로 꺼내지는지 확인해보자

 

노드 서버에서 console.log 로 출력하면 터미널 창에 출력되니 브라우저에서 위 URL을 입력해 요청을 보낸 뒤 터미널 창을 보면 클라이언트가 보낸 값이 출력될 것


<< 클라이언트에게 쿠키 보내기 >>

클라이언트에게 쿠키를 보낼 때는 다음과 같이 함

 

아래 코드를 컨트롤러에 덮어 씌우자

const express = require('express');
const app = express();
const port = 3001;

app.get('/', (req, res) => {
    const cookieOptions = {
        expires: new Date(Date.now() + 3600000)
    };
    
    res.cookie('myCookie', '12345', cookieOptions);

    res.sendStatus(200);
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

(1). 이름이 myCookie인 쿠키를 생성해 그 안에 12345 를 저장

(2). 쿠키의 옵션으로 만료 시간 지정

  만료 시간은 밀리초 단위로 여기서 설정한 만료 시간은 1시간(60 * 60 * 1000) 뒤 임

 

노드 제이에스 서버를 재시작 한 후 브라우저의 주소창에 http://localhost:3001/ 를 입력해 접근해보자

쿠키는 당연히 눈에 보이지 않으니 개발자 도구로 확인해야함

브라우저 내 개발자 도구 -> 응용 프로그램(1) -> 쿠키(2) 를 확인해보자


<< 세션에 데이터 저장하기 >>

 

노드 제이에스 서버에서 세션을 사용하려면 세션 라이브러리를 설치해야함

서버를 중지 시키고 세션 라이브러리를 설치하자

(1). 세션 라이브러리를 설치하는 명령

 

이제 컨트롤러 파일에 아래 코드를 덮어 씌우자

const express = require('express');
const session = require('express-session');
const app = express();
const port = 3001;

app.use(session({
    secret: 'key',
    resave: false,
    saveUninitialized: true
}));

app.get('/', (req, res) => {
    if(req.session.userId === undefined) {
        req.session.userId = 12345;
    } else {
        console.log(req.session.userId);
    }

    res.sendStatus(200);
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`);
});

<< 코드 설명 >>

(1). 세션을 사용하기 위한 import

(2). 세션의 보안 관련 설정

(3). 세션이 제대로 동작하는지 확인하기 위한 if문으로 세션이 제대로 동작한다면 두 번째 접근 부터는 터미널에 userId에 저장해둔 값인 12345 가 출력될 것

 

이제 서버를 재시작하고 브라우저의 주소창에 http://localhost:3001/ 를 입력해 접근해보자


여기까지 이번 Chapter에서 배울 내용을 위한 최소한의 노드 제이에스 서버 관련된 것들을 배웠음

이 글에 있는 내용을 이해하고 싶다면 별도로 노드 제이에스 또는 JSP/Servlet 관련 서적으로 서버에 대한 내용을 깊게 배우면 됨

728x90
LIST

<< 학습 목표 >>

1. 컴포넌트 합성을 사용할 수 있다.


컴포넌트 합성(Composition) 이란 컴포넌트 안에 다른 컴포넌트가 들어가는 것을 뜻함

 

우리가 지금까지 만든 주소록 프로젝트도 컴포넌트 합성을 사용했음

AddressMng 부모 컴포넌트 안에 AddressAdd, AddressList 자식 컴포넌트가 들어있음

 

이와 같은 방법도 컴포넌트 합성이긴 하지만 리엑트에서 말하는 진짜 컴포넌트 합성을 사용해보자

프로젝트 -> src -> chapter04 -> AddressMng.jsx 파일 내 코드를 아래와 같이 수정하자

import React, {useState} from 'react';
import AddressAdd from './AddressAdd';
import AddressList from './AddressList';

function ProjectTitle(props) {
    return (
    <div className="text-center">
        {props.children}
    </div>
    );
}

function AddressMng() {
    const [addressInfoList, setAddressInfoList] = useState([]);

    function appendAddressInfo(addressInfo) {
        let newAddressInfoList = Array.from(addressInfoList);
        newAddressInfoList.push(addressInfo);

        setAddressInfoList(newAddressInfoList);
    }

    return(
        <div className="container">
            <ProjectTitle>
                <h1>[ 주소록 관리 프로젝트 ]</h1>
                <hr />
            </ProjectTitle>

            <AddressAdd appendAddressInfo={appendAddressInfo} />

            <AddressList addressInfoList={addressInfoList}/>
        </div>
    );
}

export default AddressMng;

<< 코드 설명 >>

컴포넌트 합성을 사용하지 않았을 때와 사용했을 때를 비교 해보면 [ 주소록 관리 프로젝트 ] 라는 프로젝트 타이틀 부분에 컴포넌트 합성을 사용한 것

<< 코드 설명 >>

AddressMng.jsx 파일 내 컴포넌트가 두 개가 됐음

(1). ProjectTitle

(2). AddressMng

AddressMng 컴포넌트는 마지막 줄에 export (3) 하고 있지만 ProjectTitle 컴포넌트는 파일 내 어디에서도 export 를 하고 있지 않음

따라서 AddressMng 컴포넌트는 다른 컴포넌트에서 import 해 출력할 수 있지만 ProjectTItle 컴포넌트는 다른 컴포넌트에서 import 할 수 없음

즉, AddressMng.jsx 파일 내에서만 사용할 컴포넌트 라는 것

 

(1). ProjectTitle 컴포넌트 안을 보면 props.children 을 출력하는데 사용(4)하고 있는데 children 속성은 우리(개발자) 가 props에 넣는게 아니라 리엑트가 props 에 넣어주는 것

(5). ProjectTitle 컴포넌트를 사용했는데 사이에 태그가 들어있음

  이렇게 컴포넌트 사이에 태그가 들어있다면 리엑트는 ProjectTItle 컴포넌트의 props의 children 속성에 사이에 들어있는 태그를 담아서 전달함

 

(1) 에서 컴포넌트 합성을 적용해 컴포넌트를 선언한 것

(5) 에서 컴포넌트 합성이 적용된 컴포넌트를 사용한 것


컴포넌트 합성은 어떻게 활용될까?

주소록 프로젝트의 규모가 작아서 컴포넌트들이 모두 상이함

그러나 포트폴리오처럼 프로젝트 규모가 커지면 컴포넌트 사이에 구조는 같지만 텍스트나 일부만 살짝 달라지는 상황이 자주 발생함

그럴 때 태그를 복사 & 붙여넣기 하는것 보다 컴포넌트 합성을 사용해 구조는 같게 하고 텍스트나 일부가 살짝 달라지는 부분을 props.children 으로 출력하도록 한다면 더 간편하고 효율적으로 컴포넌트를 관리할 수 있음

 

위에서 한 컴포넌트 합성 방법을 Containment 라고 부름

728x90
LIST

<< 학습 목표 >>

1. State 끌어올리기를 구현할 수 있다.


Chapter03 에서 프로젝트를 하며 Context 를 사용해 프로젝트 내 컴포넌트들이 데이터를 공유해 사용하는 방법을 배웠음

이번에는 useState 훅을 사용해 인접한 컴포넌트 사이에 데이터를 공유해 사용하는 방법을 배워보자

 

아래와 같이 부모 컴포넌트는 value 변수를 갖고 있고 value 변수에는 2가 들어있음

자식 컴포넌트 A는 부모 컴포넌트가 갖고 있는 value 변수 값에 2를 곱해 출력하는 컴포넌트

자식 컴포넌트 B는 부모 컴포넌트가 갖고 있는 value 변수 값에 3을 곱해 출력하는 컴포넌트

이런 상황이라면 부모 컴포넌트에서 자식 컴포넌트의 props 로 value 를 전달해 해결할 수 있다는 우리는 걸 잘 알고 있음

이렇게 세 컴포넌트가 하나의 값을 사용하는데 이때 value 변수를 shared state / 공유 변수라고 함

 

공유 변수라는 말이 붙었지만 전혀 새로울 게 없는 내용임


 

Chapter03에서 진행했던 주소록 프로젝트에서 주소록을 추가하는 부분만 지금 배운 공유 변수를 적용한 코드로 바꿔보자

Context와 useEffect가 빠진 훨씬 더 가벼운 프로젝트가 됨

 

코드가 많이 바뀌었으니 프로젝트 -> src -> chapter04 -> AddressMng.jsx 파일을 추가하고 아래 코드를 추가하자

import React, {useState} from 'react';
import AddressAdd from './AddressAdd';
import AddressList from './AddressList';

function AddressMng() {
    const [addressInfoList, setAddressInfoList] = useState([]);

    function appendAddressInfo(addressInfo) {
        let newAddressInfoList = Array.from(addressInfoList);
        newAddressInfoList.push(addressInfo);

        setAddressInfoList(newAddressInfoList);
    }

    return(
        <div className="container">
            <div className="text-center">
                <h1>[ 주소록 관리 프로젝트 ]</h1>
                <hr />
            </div>

            <AddressAdd appendAddressInfo={appendAddressInfo} />

            <AddressList addressInfoList={addressInfoList}/>
        </div>
    );
}

export default AddressMng;

<< 코드 설명 >>

(1). 주소록을 저장할 변수 선언

(2). 주소록에 새로운 주소 정보를 추가하는 함수

  전에는 useEffect 훅이 주소 정보를 감지하고 있다가 주소 정보가 바뀌었다면 useEffect 안에서 주소록에 새로운 주소 정보를 추가했지만 이제는 이 함수가 그러한 역할을 함

  단, 함수이므로 주소 정보를 감지하지는 않음

(3). 주소 정보 추가 컴포넌트로 주소 정보를 추가할 수 있는 함수를 넘김

(4). 주소록 출력 컴포넌트로 주소록을 넘김

 

 

프로젝트 -> src -> chapter04 -> AddressAdd.jsx 파일을 추가하고 아래 코드를 추가하자

import React, {useState} from 'react';

function AddressAdd(props) {
    const [nameVal, setNameVal] = useState();
    const [telVal, setTelVal] = useState();
    const [addressVal, setAddressVal] = useState();
    const appendAddressInfo = props.appendAddressInfo;

    let clickAddBtn = () => {
        let addressInfo = {
            "name": nameVal,
            "tel": telVal,
            "address": addressVal,
            "rdate": new Date().getTime()
        }

        appendAddressInfo(addressInfo);
    }

    return(
        <div id="add_panel">
            <div className="row align-items-center justify-content-center">
                <div className="col-2">
                    <input type="text" className="form-control" placeholder="이름" onChange={(e) => setNameVal(e.target.value)} />
                </div>
                <div className="col-2">
                    <input type="tel" className="form-control" placeholder="연락처"  onChange={(e) => setTelVal(e.target.value)} />
                </div>
                <div className="col-5">
                    <input type="text" className="form-control" placeholder="주소" onChange={(e) => setAddressVal(e.target.value)} />
                </div>
                <div className="col-2 text-center">
                    <button type="button" className="btn btn-primary" onClick={clickAddBtn}>추가</button>
                </div>
            </div>
            <hr />
        </div>
    );
}

export default AddressAdd;

<< 코드 설명 >>

여기는 바뀐 부분이 크지 않음

(1). 부모 컴포넌트가 전달한 주소록 추가 함수를 꺼냄

(2). 추가 버튼 클릭 시 주소록에 새로운 주소 정보 추가

 

 

프로젝트 -> src -> chapter04 -> AddressList.jsx 파일 추가 후 아래 코드 추가

import React from 'react';
import Address from './Address';

function AddressList(props) {
    const addressInfoList = props.addressInfoList;

    return(
        <div id="list_panel">
            <table className="table text-center">
                <thead>
                    <tr className="row">
                        <th className="col-1" scope="col"></th>
                        <th className="col-2" scope="col">이름</th>
                        <th className="col-2" scope="col">연락처</th>
                        <th className="col-5" scope="col">주소</th>
                        <th className="col-2" scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {
                    addressInfoList.map((addressInfo, index) => {
                        return <Address
                                    key={addressInfo.rdate+"_"+index}
                                    index={index}
                                    addressInfo={addressInfo}
                                />
                    })
                    }
                </tbody>
            </table>
        </div>
    );
}

export default AddressList

<< 코드 설명 >>

역시 바뀐 부분이 크지 않음

(1). 부모 컴포넌트가 전달한 주소록을 꺼냄

(2). 주소록을 출력

  여기서 살짝 달라진게 있다면 주소 정보를 통째로 전달하고 있다는 점

  그러나 잘 알고 있듯 전혀 특별하진 않음

 

 

프로젝트 -> src -> chapter04 -> Address.jsx 파일 추가 후 아래 코드 추가

import React from 'react';

function Address(props) {
    const addressInfo = props.addressInfo;

    return(
        <tr className="row">
            <th className="col-1" scope="row">{props.index+1}</th>
            <td className="col-2">{addressInfo.name}</td>
            <td className="col-2">{addressInfo.tel}</td>
            <td className="col-5 text-left">{addressInfo.address}</td>
            <td className="col-2">
                <div className="btn-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-success">수정</button>
                    <button type="button" className="btn btn-danger">삭제</button>
                </div>
            </td>
        </tr>
    );
}

export default Address;

<< 코드 설명 >>

(1). AddressList 부모 컴포넌트가 전달한 주소 정보를 꺼냄


chapter03의 주소록 프로젝트와 여기(chapter04)의 주소록 프로젝트의 차이점은 useEffect 유무

useEffect 훅을 사용해 변수의 변화를 감지할 수도 있지만 useEffect 훅을 사용하지 않아도 변수의 변화를 감지할 수 있음

chapter03에서도 설명했듯이 컴포넌트가 갖고 있는 변수에 변화가 생기면 리엑트는 컴포넌트 내 변수의 값을 바꾸는게 아닌 바뀐 값을 갖고 있는 새로운 컴포넌트를 만들어 교체하기 때문임

 

여기서 AddressAdd 컴포넌트에서 AddressMng 부모 컴포넌트가 갖고 있는 addressInfoList에 변화를 주는데 이렇게 자식 컴포넌트가 부모 컴포넌트의 변수에 변화를 주는 걸 State 끌어올리기 라고 부름

 

주소록 수정과 삭제도 State 끌어올리기 를 사용할 수 있으니 그 부분은 여러분이 직접 해보자

728x90
LIST

<< 학습 목표 >>

1. 리엑트 라우터(Router) 라이브러리가 필요한 이유를 설명할 수 있다.

2. 리엑트 라우터(Router) 라이브러리를 사용해 SPA를 구현할 수 있다.

3. URL 파라미터를 사용할 수 있다.


리엑트가 SPA ( Single Page Application ) 을 구현할 수 있다고 했는데 지금까지는 페이지 하나에서 모두 처리했음

이번에는 리엑트로 SPA를 구현할 수 있는 라우터(Router) 를 배워보자

 

라우터를 사용하려면 프로젝트에 react-router-dom 라이브러리를 설치해야함

vs code 에서 터미널을 열고 프로젝트로 이동한 후 프로젝트에 react-router-dom 라이브러리를 설치하자

1. 프로젝트로 이동 : cd study-project

2. react-router-dom 라이브러리 설치 : npm install react-router-dom


리엑트 라우터는 브라우저의 URL 경로를 사용하여 특정 컴포넌트를 불러오는(렌더링하는) 라이브러리임

 

라우터를 사용하는 이유는 다음과 같음

1. 브라우저의 URL 경로를 사용하여 컴포넌트를 보여줄 수 있음

2. SPA는 모든 컨텐츠(컴포넌트)를 하나의 페이지에서 보여줌

  이러한 SPA를 구현하려면 브라우저의 URL 경로를 사용하여 컴포넌트를 보여줘야함

  리엑트 라우터는 이러한 SPA를 구현하는 데 필요한 기능을 제공함

3. 리엑트 라우터를 사용하면 중첩된 컴포넌트 간의 라우팅을 처리할 수 있음

4. 리엑트 라우터는 브라우저의 히스토리를 관리할 수 있음

  이를 통해 뒤로가기, 앞으로가기 버튼 등을 사용하여 페이지 이동을 처리할 수 있음

 

리엑트 라우터의 장점은 다음과 같음

1. SPA 구현이 용이함

2. 중첩된 라우팅을 지원함

3. 히스토리 관리가 가능함

4. 동적 라우팅을 지원함

 

리엑트 라우터의 단점은 다음과 같음

1. 학습을 하기 위해 시간이 다소 소요될 수 있음

2. 라우터를 남용하면 프로젝트가 필요 이상으로 복잡해질 수 있음

3. SEO 최적화가 어려움

  SEO란 검색 엔진 최적화인데 검색 사이트에서 검색이 되고 상위에 노출될 수 있도록 하는 작업을 뜻함

  이를 위해서 많은 사이트에서 각 컨텐츠(페이지, 컴포넌트)별로 페이지 내 다양한 키워드를 넣어둠

  그러나 리엑트 라우터는 SPA 이므로 하나의 페이지에서 모든 컨텐츠를 다 보여줘 각 페이지별 다양한 키워드를 넣기 어렵다는 점 등 SEO 최적화가 어렵다는 단점이 있음

 

SPA를 구현하기 위해서는 라우터 라이브러리가 필수이지만 단점도 있으니 단점을 해결할 방안도 찾는것이 좋음


이제 리엑트 라우터를 실습하자

라우터를 실습하기 위해 프로젝트 -> src -> chapter04 폴더 내 아래와 같은 컴포넌트들을 추가할 것

1. Component02.jsx : 브라우저의 URL에 맞는 페이지를 불러올 컴포넌트

2. Home.jsx : 브라우저의 URL이 / 일 경우 보여줄 컴포넌트

3. About.jsx : 브라우저의 URL이 /about 일 경우 보여줄 컴포넌트

4. List.jsx : 브라우저의 URL이 /topics 일 경우 보여줄 컴포넌트

 

<< 1. Component02.jsx >>

import React from 'react';

function Component02() {
    return(
        <div style={{margin: "20px"}}>
            <nav>
                <span>Home</span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span>About</span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span>List</span>
            </nav>

            <div>
                {/* URL에 맞는 컴포넌트를 보여줄 영역 */}
            </div>
        </div>
    );
}

export default Component02;

위 컴포넌트에서 nav 태그가 사이트 메뉴 역할을 함

 

nav 태그 밑에 있는 div 태그가 메뉴에 맞는 컴포넌트를 보여줄 태그임

HTML만 사용했다면 nav 태그 내 span 태그에 a 태그를 넣어야하지만 리엑트 라우터의 경우 Link 컴포넌트를 사용함

<< 코드 설명 >>

(1). Link 컴포넌트를 사용하기 위해서는 import 를 해야함

(2). 링크가 필요한 곳에 a 태그 대신 Link 컴포넌트를 사용함

  Link 태그의 to 속성이 a 태그의 href 속성과 같은 역할을 함

 

 

브라우저 URL에 맞는 컨텐츠를 보여줄 영역에는 Routes 컴포넌트와 Route 컴포넌트를 사용함

<< 코드 설명 >>

(1). Routes, Route 컴포넌트를 사용하기 위해서는 각 컴포넌트를 import 해야함

(2). Route 컴포넌트를 사용해 브라우저 URL에 맞는(path 속성) 컴포넌트를 불러오도록 설정함(element 속성)

  그 후 Route 컴포넌트들을 Routes 컴포넌트로 감싸야함

 

 

Component02 컴포넌트의 마지막으로 Link 컴포넌트들과 Routes 컴포넌트를 BrowserRouter 컴포넌트로 감싸야함

< < 코드 설명 >>

(1). BrowserRouter 컴포넌트를 사용하기 위해서는 import를 해야함

(2). Link 컴포넌트부터 Routes 컴포넌트까지 BrowserRouter 컴포넌트로 감싸야 Link, Routes 컴포넌트가 동작함


이제 브라우저 URL에 따라 페이지에 보여줄 컴포넌트들을 추가하자

컴포넌트는 아래와 같이 간단하게 추가할 것임

프로젝트 -> src -> chapter04 -> Home.jsx

import React from 'react';

function Home() {
    return(
        <div>
            <h1>Home 컴포넌트</h1>
        </div>
    );
}

export default Home;

 

프로젝트 -> src -> chapter04 -> About.jsx

import React from 'react';

function About() {
    return(
        <div>
            <h1>About 컴포넌트</h1>
        </div>
    );
}

export default About;

 

프로젝트 -> src -> chapter04 -> List.jsx

import React from 'react';

function List() {
    return(
        <div>
            <h1>List 컴포넌트</h1>
        </div>
    );
}

export default List;

 

Component02 컴포넌트에 화면에 보여줄 컴포넌트들을 불러오자(1)

 

Component02 컴포넌트를 웹 페이지에 출력해보자

각 메뉴를 클릭하면 URL이 이동하면서 URL에 맞는 각 컴포넌트들이 보임

 

지금 페이지들의 링크 구조를 보면 다음과 같음

 


페이지들의 링크 구조를 다음과 같이 추가하려면 어떻게 해야할까?

즉, 지금은 Component02 컴포넌트에서 링크를 눌러 Home, About, List 컴포넌트를 불러오고 있는 구조임

여기에 List 컴포넌트에 링크를 추가해 List 컴포넌트에서 링크를 눌러 part1, part2 컴포넌트로 이동하는 구조를 추가하고 싶음

 

그럴 때는 우선 List 컴포넌트에 이동할 수 있게 링크를 추가해야함

<< List 컴포넌트 소스 코드 >>

더보기

import React from 'react';
import { Link } from 'react-router-dom';

function List() {
    return(
        <div>
            <h1>List 컴포넌트</h1>

            <ul>
                <li><Link to="/list/part1">Part1</Link></li>
                <li><Link to="/list/part2">Part2</Link></li>
            </ul>
        </div>
    );
}

export default List;

 

그 후 Component02 컴포넌트의 Routes 안에 Route 컴포넌트로 URL에 맞는 컴포넌트를 보여주도록 할 수 있음

<< Component02 컴포넌트 소스 코드 >>

더보기

import React from 'react';
import { Link, Routes, Route, BrowserRouter } from 'react-router-dom';
import Home from './Home';
import About from './About';
import List from './List';
import ListPart1 from './ListPart1';
import ListPart2 from './ListPart2';

function Component02() {
    return(
        <div style={{margin: "20px"}}>
            <BrowserRouter>
            <nav>
                <span><Link to="/">Home</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/about">About</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/list">List</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
            </nav>

            <div>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/list" element={<List />} />
                    <Route path="/list/part1" element={<ListPart1 />} />
                    <Route path="/list/part2" element={<ListPart2 />} />
                </Routes>
            </div>
            </BrowserRouter>
        </div>
    );
}

export default Component02;


컴포넌트가 GET 파라미터 값을 전달 받았을 때 컴포넌트에서 전달 받은 GET 파라미터는 어떻게 꺼낼까?

다음과 같이 useLocation 훅을 사용해 꺼낼 수 있음

 

<< List 컴포넌트 >>

import React from 'react';
import { Link, useLocation } from 'react-router-dom';

function List() {
    const location = useLocation();
    const parameters = new URLSearchParams(location.search);
    const value = parameters.get("name");

    return(
        <div>
            <h1>List 컴포넌트</h1>
            { value != null && <p>전달 받은 name 파라미터의 값은 {value} 입니다</p> }
            <ul>
                <li><Link to="/list/part1">Part1</Link></li>
                <li><Link to="/list/part2">Part2</Link></li>
            </ul>
        </div>
    );
}

export default List;

 

<< 코드 설명 >>

(1). useLocation 훅을 사용하기 위한 import

(2). useLocation 훅을 사용해 JS의 location 객체를 리엑트로 가져옴

(3). URLSearchParams 클래스를 사용해 location 객체가 가지고 있는 GET 파라미터 값들을 꺼내기 쉽게 변환함

  변환한 후 parameters 변수에 저장

(4). get 함수를 사용해 parameters 변수에 저장된 파라미터 값을 꺼내 value 변수에 저장

  이때 꺼낼 파라미터의 이름을 인자로 넣음

  위 코드는 GET 파라미터로 name=값 으로 전달했기 때문에 name 파라미터 값을 꺼내는 코드임

(5). 꺼낸 GET 파라미터를 출력함


또한 URL의 경로 중 일부를 파라미터로 사용할 수도 있음

 

Component02 컴포넌트에 URL의 경로 중 일부를 파라미터로 사용할 수 있도록 설정해보자

<< Component02 컴포넌트 소스 코드 >>

import React from 'react';
import { Link, Routes, Route, BrowserRouter } from 'react-router-dom';
import Home from './Home';
import About from './About';
import List from './List';
import ListPart1 from './ListPart1';
import ListPart2 from './ListPart2';
import AboutWho from './AboutWho';

function Component02() {
    return(
        <div style={{margin: "20px"}}>
            <BrowserRouter>
            <nav>
                <span><Link to="/">Home</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/about">About</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/about/me">About Me</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/about/you">About Your</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
                <span><Link to="/list">List</Link></span>&nbsp;&nbsp;&nbsp;&nbsp;
            </nav>

            <div>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/about/:who" element={<AboutWho />} />
                    <Route path="/list" element={<List />} />
                    <Route path="/list/part1" element={<ListPart1 />} />
                    <Route path="/list/part2" element={<ListPart2 />} />
                </Routes>
            </div>
            </BrowserRouter>
        </div>
    );
}

export default Component02;

<< 코드 설명 >>

(1). about URL 다음에 /me 또는 /you 경로를 더 붙인 링크를 추가했음

(2). /about/me, /about/you 를 /list/part1, /list/part2 처럼 처리할 수도 있지만 /about/me, /about/you 는 me, you 를 파라미터로 사용하기 위해 path가 /about/:who 인 Route 컴포넌트를 추가

  :who 가 아니어도 됨 :a, :b 와 같이 아무 문자나 가능하나 위와 같이 의미 있는 문자를 사용하는게 좋음

  /about/me, /about/you 모두 (2)의 Route가 지정한 컴포넌트인 AboutWho 컴포넌트가 보임

 

프로젝트 -> src -> Chapter04 -> AboutWho.jsx 를 추가하고 아래 코드를 추가하자

import React from 'react';
import { useParams } from 'react-router-dom';

function AboutWho() {
    const {who} = useParams();

    return(
        <div>
            <h1>About</h1>
            <h3>{who} 입니다</h3>
        </div>
    );
}

export default AboutWho;

<< 코드 설명 >>

(1). URL 중 일부를 파라미터로 사용하기 위해서는 useParams 훅을 import 해야함

(2). URL 중 일부를 파라미터로 가져오기 위해 useParams 훅을 사용했음

  Component02 컴포넌트 소스 코드 에서 봤던 me 또는 you 가 who 변수에 저장됨

(3). who 변수에 저장한 파라미터를 출력

728x90
LIST

<< 학습 목표 >>

1. 입력 폼에 기본값을 갖도록 설정할 수 있다.

2. 입력 폼이 기본값을 가지면서 변경되도록 설정할 수 있다.


리엑트로 입력 폼(form)을 핸들링 하는 방법은 지금까지 우리가 중간 중간 배운대로만 알아도 됨

가끔 필요한 경우가 있는 것들만 간단하게 알아보자

 

우선 프로젝트 -> src -> chapter04 폴더를 만들자

그 후 chapter04 폴더 내 Component01.jsx 파일을 추가하고 아래 코드를 추가하자

 

<< 기본 값이 들어있는 입력 폼 >>

1. 한 줄 입력

  간단하게 한 줄로 입력할 수 있는 input 태그의 경우 HTML과 동일하게 value 속성을 사용해서 기본값을 가지고 있는 한 줄 입력 폼을 구성할 수 있음

 

2. 여러 줄 입력

  여러 줄로 입력할 수 있는 textarea 태그의 경우 HTML과 다르게 value 속성을 사용해서 기본값을 가지고 있는 여러 줄 입력 폼을 구성할 수 있음

 

한 줄, 여러 줄 입력 시 당연히 JSX를 사용할 수 있음

여기서 주의할 점은 위와 같이 기본값을 갖도록 하면 기본값을 갖긴 하지만 입력을 할 수는 없음

위와 같이 컴포넌트를 수정 한 후 출력해 확인해보자

이를 활용하면 회원 정보 수정 시 아이디는 바꿀 수 없게 만들 수 있음

 

기본값도 가지면서 입력을 허용하려면 useState 훅과 onChange 이벤트 핸들러를 사용해야함

 

3. 체크박스, 라디오버튼

  체크박스와 라디오 버튼의 경우 HTML과 동일하게 checked 속성을 사용해 기본값이 체크되도록 할 수 있음

역시나 체크박스와 라디오 버튼을 위와 같이 설정한다면 체크를 풀 수 없는 상태가 됨

기본값이 체크된 상태로 다른 항목이 체크 되도록 하려면 useState 훅과 onChange 이벤트 핸들러를 사용해야함

또한 조건부 렌더링도 함께 사용해야함

 

체크박스와 라디오 버튼의 경우 다른 항목이 체크 되도록 하는 방식이 다르니 따로 보자

3-1. 체크박스가 기본값을 체크하면서 다른 항목도 체크하도록 만들고 싶을 때

  체크 박스가 기본값을 체크하면서 다른 항목도 체크하도록 만들고 싶을 때는 체크 박스의 각 항목별 상태를 저장할 변수를 선언(1)해야함

체크 박스는 체크했음(ture), 체크안했음(false)으로 핸들링 할 수 있으니 기본값을 체크하기 위해서 기본값의 상태를 저장할 변수에 true를 저장하고 나머지 항목들은 false를 저장하면 됨

체크 박스에서는 checked 속성을 통해 기본값을 체크하도록 핸들링할 수 있음(2)

또한 체크 박스의 체크 상태가 변했을 때 호출할 함수를 이벤트 핸들러로 등록해야함(2)

체크 박스의 체크 상태가 변했을 때 setter 함수를 사용해 체크 상태를 저장(3) 하도록 하면 기본값이 체크 된 상태로 다른 항목들도 체크할 수 있음

 

3-2. 라디오 버튼이 기본값을 체크하면서 다른 항목을 체크하도록 만들고 싶을 때

  라디오 버튼은 항목 중 하나를 선택하는 버튼이므로 기본값 또는 현재 선택한 항목의 값을 저장하기 위해 변수를 하나만 선언함(1)

라디오 버튼 역시 체크박스처럼 checked 속성을 통해 기본값을 체크하도록 핸들링할 수 있음(2)

또한 라디오 버튼의 체크 상태가 변했을 때 호출할 함수를 이벤트 핸들러로 등록해야함(2)

체크박스의 상태가 변했을 때 체크한 항목의 값으로 변경하도록 함(3)

 

4. select

select 의 경우 HTML과 동일하게 value 속성을 사용해서 기본값이 체크되도록 할 수 있음

단, 지금까지 봐온것처럼 위와 같이 한다면 기본값을 바꿀 수 없음

select의 기본 값을 바꾸도록 하려면 useState 훅과 onChange 이벤트 핸들러를 사용해야함

 

각 입력 폼의 기본값 설정 방법과 값을 바꿀 때 설정 방법들이 비슷한듯 다르니 잘 기억해두기

728x90
LIST

<< 학습 목표 >>

1. useState 훅을 활용할 수 있다.

2. 조건부 렌더링을 활용할 수 있다.

3. 리엑트의 렌더링 방식을 설명할 수 있다.


리엑트 프로젝트의 마지막, 주소록 수정과 삭제를 구현하자

1. 주소록 수정

  주소 정보에 있는 [ 수정 ] 버튼 클릭 시 이름, 연락처, 주소가 수정 가능한 상태로 바뀜(1)

  이름, 연락처, 주소가 수정 가능 상태로 바뀔 때 현재 이름, 연락처, 주소가 입력된 상태임(1)

  [ 수정 ] 버튼은 [ 완료 ] 버튼으로 바뀌어야함(1)

  이름, 연락처, 주소를 수정 한 뒤 [ 완료 ] 버튼을 누르면 이름, 연락처, 주소가 수정 불가 상태로 바뀜(1)

  이름, 연락처, 주소가 수정 불가 상태로 바뀔 때 바꾼 이름, 연락처, 주소로 변경돼 보여야함(1)

   [ 완료 ] 버튼은 [ 수정 ] 버튼으로 바뀌어야함(1)

 

위 내용과 이미지를 참고해 주소록 수정 기능을 구현해보자

 

<< 1단계 힌트 >>

더보기

<< Address 컴포넌트 >>

import React, {useState} from 'react';

function Address(props) {
    const [isEditing, setIsEditing] = useState(false);

    let clickUpdateBtn = () => {
        setIsEditing(!isEditing);
    }

    let clickUpdateDoneBtn = () => {
        setIsEditing(!isEditing);
    }

    return(
        <tr className="row">
            <th className="col-1">{props.number}</th>
            <td className="col-2">{props.name}</td>
            <td className="col-2">{props.tel}</td>
            <td className="col-5 text-left">{props.address}</td>
            <td className="col-2">
                <div className="btn-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
                    <button type="button" className="btn btn-danger" >삭제</button>
                </div>
            </td>
        </tr>
    );
}

export default Address;

 

<< 코드 설명 >>

 

(1). isEditing 변수를 선언해 수정 상태인지 아닌지를 저장함

(2). isEditing 변수를 사용해 조건부 렌더링 및 onclick 이벤트 핸들러 설정

  수정 중(isEditing=true) 이라면 <button ...중략... onClick=clickUpdateDoneBtn>완료</button> 이 됨

  수정 아님(isEditing=false) 라면 <button ...중략 ... onClick=clickUpdateBtn>수정</button> 이 됨


<< 2단계 힌트 >>

더보기

<< Address 컴포넌트 >>

import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';

function Address(props) {
    const setAddress = useContext(AddressContext).setAddress;
    const [isEditing, setIsEditing] = useState(false);

    let clickUpdateBtn = () => {
        setIsEditing(!isEditing);
    }

    let clickUpdateDoneBtn = () => {

        setAddress(바뀐 주소 정보);
        setIsEditing(!isEditing);
    }

    return(
        <tr className="row">
            <th className="col-1">{props.number}</th>
            <td className="col-2">{props.name}</td>
            <td className="col-2">{props.tel}</td>
            <td className="col-5 text-left">{props.address}</td>
            <td className="col-2">
                <div className="btn-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
                    <button type="button" className="btn btn-danger" >삭제</button>
                </div>
            </td>
        </tr>
    );
}

export default Address;

 

<< 코드 설명 >>

 

(1). AddressMng 부모 컴포넌트가 만든 Context를 사용해 AddressAdd 컴포넌트와 AddressList 컴포넌트가 주소 정보를 주고 받기 때문에 Address 컴포넌트에서 변경된 주소 정보를 AddressList 컴포넌트로 전달해야함

그러기 위해서 AddressMng 부모 컴포넌트가 만든 Context을 import 함

(2). useContext 훅을 사용해 AddressMng 부모 컴포넌트가 만든 Context 내 setter를 사용하도록 설정

(3). [ 완료 ] 버튼 클릭 시 바뀐 주소 정보를 setter로 저장하면 AddressList의 useEffect 훅이 이를 감지할 수 있음


<< 3단계 힌트 >>

더보기

<< Addressd 컴포넌트 >>

import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';

function Address(props) {
    const setAddress = useContext(AddressContext).setAddress;

    const [isEditing, setIsEditing] = useState(false);
    const indexVal = useState(props.index)[0];
    const [nameVal, setNameVal] = useState(props.name);
    const [telVal, setTelVal] = useState(props.tel);
    const [addressVal, setAddressVal] = useState(props.address);

    let clickUpdateBtn = () => {
        setIsEditing(!isEditing);
    }

    let clickUpdateDoneBtn = () => {

        setAddress(바뀐 주소 정보);
        setIsEditing(!isEditing);
    }

    return(
        <tr className="row">
            <th className="col-1" scope="row">{indexVal+1}</th>
            <td className="col-2">{ isEditing ? <input type="text" className="form-control" value={nameVal} onChange={(e) => setNameVal(e.target.value)} /> : nameVal}</td>
            <td className="col-2">{ isEditing ? <input type="tel" className="form-control" value={telVal} onChange={(e) => setTelVal(e.target.value)}  /> : telVal}</td>
            <td className="col-5 text-left">{ isEditing ? <input type="text" className="form-control" value={addressVal} onChange={(e) => setAddressVal(e.target.value)} /> : addressVal}</td>
            <td className="col-2">
                <div className="btn-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
                    <button type="button" className="btn btn-danger" >삭제</button>
                </div>
            </td>
        </tr>
    );
}

export default Address;

 

<< 코드 설명 >>

 

(1). [ 수정 ] 버튼을 클릭했을 때 현재 이름, 연락처, 주소를 보여줘야하므로 AddressList 부모 컴포넌트가 전달한, 출력할, n번째 주소 정보의 이름, 연락처, 주소를 초기값으로 갖고 있는 변수 선언

(2). [ 수정 ] 버튼을 클릭했을 때는 input 태그, [ 완료 ] 버튼을 클릭했을 때는 텍스트를 보여줘야하므로 조건부 렌더링으로 지정

  [ 수정 ] 버튼 클릭 시 input 태그를 보여주는데 현재 이름, 연락처, 주소를 보여줘야하므로 value 속성 값을 현재 이름, 연락처, 주소로 지정

  이름, 연락처, 주소를 수정했을 때 수정된 이름, 연락처, 주소를 setter 함수를 사용해 nameVal, telVal, addressVal 변수에 저장

 

여기서 특징적인 부분은 (1)의 indexVal 인데 indexVal은 useState 훅을 사용했는데 기존과는 다르게 사용하고 있음

 

이와 같이 사용하면 "해당 변수에 setter 함수를 사용하지 않겠다" 가 됨

즉, "해당 변수의 값은 변경할 일이 없다" 라는 것

 indexVal 변수에 저장한건 부모 컴포넌트가 props로 전달 받은 index 변수의 값임

 

indexVal 변수는 이 주소 정보가 AddressList 부모 컴포넌트가 갖고 있는 addressList에서 몇 번 인덱스 주소 정보인지 나타내는 변수로 Address 컴포넌트에서는 주소 정보 번호로 출력(1)하고 있음


<< 4단계 힌트 >>

더보기

<< Address 컴포넌트 >>

변경이 미미하므로 바로 코드 설명을 보자

 

(1). [ 완료 ] 버튼 클릭 시 수정된 주소 정보를 생성

(2). Context 로 전달 받은 setter 를 사용해 AddressList 컴포넌트로 주소 정보를 전달

  이 때 유의할 점은 AddressList 컴포넌트로 주소 정보를 전달할 때 수정한 주소 정보의 index 번호도 같이 전달하고 있다는 점


<< 최종(5단계) 힌트 >>

더보기

<< AddressList 컴포넌트 >>

import React, {useState, useEffect, useContext} from 'react';
import Address from './Address';
import { AddressContext } from './AddressMng';

function AddressList() {
    const [addressList, setAddressList] = useState([]);
    const address = useContext(AddressContext).address;

    useEffect(() => {
        if(address != null) {
            let newAddressList = Array.from(addressList);

            if(address.index !== undefined) {
                newAddressList[address.index] = address;
            } else {
                newAddressList.push(address);
            }

            setAddressList(newAddressList);
        }
    }, [address]);

    return(
        <div id="list_panel">
            <table className="table text-center">
                <thead>
                    <tr className="row">
                        <th className="col-1" scope="col"></th>
                        <th className="col-2" scope="col">이름</th>
                        <th className="col-2" scope="col">연락처</th>
                        <th className="col-5" scope="col">주소</th>
                        <th className="col-2" scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {
                    addressList.map((address, index) => {
                        return <Address
                                    key={index}
                                    index={index}
                                    name={address.name}
                                    tel={address.tel}
                                    address={address.address}
                                />
                    })
                    }
                </tbody>
            </table>
        </div>
    );
}

export default AddressList

 

<< 코드 설명 >>

(1). Address 자식 컴포넌트에서 인덱스 번호를 사용할 수 있게 props로 전달

(2). AddressAdd 컴포넌트 또는 Address 자식 컴포넌트가 주소 정보를 전달했다면 useEffect 훅이 동작함

  전달 받은 주소 정보가 있다면 기존의 주소록을 복사한 새 주소록을 만듬

  Address 자식 컴포넌트에서 주소 정보를 전달했다면 주소 정보 안에 index(수정된 주소의 인덱스 번호)가 들어있음, 주소 정보 안에 index가 있다면 기존의 주소 정보를 새로운 주소 정보로 교체

  AddressAdd 컴포넌트에서 주소 정보를 전달했다면 주소 정보 안에 index가 없음, 주소 정보 안에 index가 없다면 주소록의 마지막에 새로운 주소 정보 추가  



2. 주소록 삭제

  주소 정보에 있는 [ 삭제 ] 버튼 클릭 시 해당 주소 정보가 삭제됨

 

위 내용을 참고해 주소록 삭제 기능을 구현하자

 

<< 1단계 힌트 >>

더보기

[ 삭제 ] 버튼을 클릭 했을 때 AddressList 부모 컴포넌트로 자신의 인덱스 번호를 전달하고 AddressList 부모 컴포넌트는 Address 자식 컴포넌트가 전달한 인덱스 번호에 있는 주소 정보를 삭제해야함

 

힌트 코드는 없음


<< 2단계 힌트 >>

더보기

<< Address 컴포넌트 >>

import React, {useState, useContext} from 'react';
import { AddressContext } from './AddressMng';

function Address(props) {
    const setAddress = useContext(AddressContext).setAddress;

    const [isEditing, setIsEditing] = useState(false);
    const indexVal = useState(props.index)[0];
    const [nameVal, setNameVal] = useState(props.name);
    const [telVal, setTelVal] = useState(props.tel);
    const [addressVal, setAddressVal] = useState(props.address);

    let clickUpdateBtn = () => {
        setIsEditing(!isEditing);
    }

    let clickUpdateDoneBtn = () => {
        let newAddress = {
            "index": indexVal,
            "name": nameVal,
            "tel": telVal,
            "address": addressVal,
            "status": "update"
        }

        setAddress(newAddress);
        setIsEditing(!isEditing);
    }

    let clickDeleteBtn = () => {
        let newAddress ={
            "index": indexVal,
            "status": "delete"
        }

        setAddress(newAddress);
    }

    return(
        <tr className="row">
            <th className="col-1" scope="row">{indexVal+1}</th>
            <td className="col-2">{ isEditing ? <input type="text" className="form-control" value={nameVal} onChange={(e) => setNameVal(e.target.value)} /> : nameVal}</td>
            <td className="col-2">{ isEditing ? <input type="tel" className="form-control" value={telVal} onChange={(e) => setTelVal(e.target.value)}  /> : telVal}</td>
            <td className="col-5 text-left">{ isEditing ? <input type="text" className="form-control" value={addressVal} onChange={(e) => setAddressVal(e.target.value)} /> : addressVal}</td>
            <td className="col-2">
                <div className="btn-group" role="group" aria-label="Basic example">
                    <button type="button" className="btn btn-success" onClick={ isEditing ? clickUpdateDoneBtn : clickUpdateBtn}>{ isEditing ? "완료" : "수정"}</button>
                    <button type="button" className="btn btn-danger" onClick={clickDeleteBtn} >삭제</button>
                </div>
            </td>
        </tr>
    );
}

export default Address;

<< 코드 설명 >>

 

이 방법은 AddressMng 부모 컴포넌트가 만든 Context 를 활용하는 방법

(1). 삭제 버튼을 클릭했을 때 함수가 호출되도록 이벤트 핸들러 추가

(2). AddressList 부모 컴포넌트로 삭제 시그널을 보내기 위해 status가 delete인 주소 정보를 생성해 setter 를 사용해 AddressList 부모 컴포넌트로 전달

  몇 번 인덱스의 주소 정보를 삭제할 지 인덱스 번호도 함께 담아 전달함

(3). 이에 맞춰 AddressList 부모 컴포넌트로 수정 시그널을 보내야하므로 주소를 수정하기 위해 생성하는 주소 정보에 status를 update로 담도록 수정

 

<< AddressList 컴포넌트 >>

import React, {useState, useEffect, useContext} from 'react';
import Address from './Address';
import { AddressContext } from './AddressMng';

function AddressList() {
    const [addressList, setAddressList] = useState([]);
    const address = useContext(AddressContext).address;

    useEffect(() => {
        if(address != null) {
            let newAddressList = Array.from(addressList);

            if(address.index !== undefined) {
                if(address.status === "update") {
                    newAddressList[address.index] = address;
                } else {
                    newAddressList.splice(address.index, 1);
                }
            } else {
                newAddressList.push(address);
            }

            setAddressList(newAddressList);
        }
    }, [address]);

    return(
        <div id="list_panel">
            <table className="table text-center">
                <thead>
                    <tr className="row">
                        <th className="col-1" scope="col"></th>
                        <th className="col-2" scope="col">이름</th>
                        <th className="col-2" scope="col">연락처</th>
                        <th className="col-5" scope="col">주소</th>
                        <th className="col-2" scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {
                    addressList.map((address, index) => {
                        return <Address
                                    key={index}
                                    index={index}
                                    name={address.name}
                                    tel={address.tel}
                                    address={address.address}
                                />
                    })
                    }
                </tbody>
            </table>
        </div>
    );
}

export default AddressList

<< 코드 설명 >>

코드가 굉장히 길어졌고 useEffect 부분만 수정됐으므로 useEffect까지만 캡쳐한 이미지

(1). Context에 들어있는 address가 변하면 반응하는 useEffect 훅

 Address Add 컴포넌트와 Address 컴포넌트에서 Context에 들어있는 address를 변하게 함

(2). Address 컴포넌트에서 address를 변하게 했다면 index 프로퍼티가 들어있음

  Address Add 컴포넌트에서 address를 변하게 했다면 index 프로퍼티가 들어있지 않음

(3). Address Add 컴포넌트에서 주소 정보를 추가했다면 주소록의 마지막에 주소 정보를 추가함

(4). Address 컴포넌트에서 수정 상태에서 [ 완료 ] 버튼을 클릭했다면 status 프로퍼티의 값이 update이므로 Address 컴포넌트에서 주소 정보 수정 시그널을 보냈다면 이라고 해석할 수 있음

  Address 컴포넌트에서 주소 정보 수정 시그널을 보냈다면 해당 자리에 들어있는 주소 정보를 새로운 주소 정보로 교체

(5). Address 컴포넌트에서 [ 삭제 ] 버튼을 클릭했다면 status 프로퍼티의 값이 delete이므로 Address 컴포넌트에서 주소 정보 삭제 시그널을 보냈다면 이라고 해석할 수 있음

  Address 컴포넌트에서 주소 정보 삭제 시그널을 보냈다면 해당 자리에 들어있는 주소 정보를 삭제


 

이제 [ 삭제 ] 버튼을 클릭해 주소 정보를 삭제해보자

그. 러. 나. 내가 원하는 주소 정보가 삭제 되지 않고 마지막에 있는 주소 정보만 삭제됨

왜그럴까??

더보기

리엑트는 부모 컴포넌트에서 map 함수로 자식 컴포넌트를 출력할 때 최소한의 동작만으로 빠르게 출력하기 위해 key 를 사용함

주소록 관리 프로젝트의 시작 ( https://codingaja.tistory.com/83 ) 에서 간단하게 언급만 했는데 "map 함수를 사용해 컴포넌트를 출력할 때 컴포넌트에 반드시 key를 전달해야하며 key는 컴포넌트 간에 구분할 수 있는 유일한 값이어야함" 라고 했음

 

다음과 같은 주소록 페이지가 되도록 주소 정보를 추가하자

 

이 상태의 웹 페이지를 시각적인 컴포넌트 트리로 그려보자

AddressList 컴포넌트의 addressList 배열 안에는 address가 서울1 ~ 서울5인 데이터들이 들어있고 이를 통해 Address 컴포넌트가 만들어짐

여기까진 우리가 알고 있는 당연한 상황임

여기서 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황이라고 해보자

그러면 address가 서울3인 Address 컴포넌트에서 AddressList 부모 컴포넌트로 삭제 시그널을 보내 useEffect 훅이 splice 함수를 사용해 addressList의 2번 인덱스(3번째) 주소를 삭제함

이때의 addressList의 상황을 보면 아래와 같음

이제 컴포넌트가 어떻게 생성될까?

 

다음과 같이 생성됨

 

리엑트는 기존의 Address 컴포넌트들의 key와 새로운 Address 컴포넌트들의 key를 비교해 key가 다른 컴포넌트만 교체함

기존의 Address 컴포넌트들 중에서 key가 4인 Address 컴포넌트가 있었는데 새로운 Address 컴포넌트들 중에서는 key가 4인 Address 컴포넌트가 없으므로 그래서 화면 상에서는 마지막 컴포넌트가 지워지는 것

결국 addressList 배열 상에서는 지우고자 하는 주소 정보가 지워졌지만 리엑트의 컴포넌트 갱신 방식 때문에 잘못된 데이터가 지워져 보이는 것


 

이 문제를 어떻게 해결해야할까??

더보기

Address 컴포넌트를 생성할 때 key를 index로 지정하면 데이터는 다르지만 index가 같은 컴포넌트가 생기는 것 때문에 발생하므로 key를 index로 사용하지 말고 각 Address 컴포넌트 마다 고유한 key를 사용하도록 해야함

말은 좀 어렵지만 리엑트를 다루는데 굉장히 중요한 내용이므로 이해해야함

만약 이해가 안된다면 [ 소플의 처음 만난 리액트 ] 책에 엘리먼트 렌더링, State와 생명주기, 리스트와 키를 읽어보자

 

이를 해결하기 위해서는 주소 정보를 [ 추가 ] 할 때 주소 정보에 추가한 날짜 정보(연, 월, 일, 시, 분, 초, 나노초) 도 같이 저장하도록 하면 됨

 

<< AddressAdd 컴포넌트 >>

 변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지

[ 추가 ] 버튼을 눌렀을 때 주소 정보를 추가한 시점인 날짜 정보도 함께 저장하도록 변경(1)

날짜 정보를 생성( new Date() ) 하고 생성한 날짜 정보의 getTime() 함수를 호출했는데 getTime() 함수는 날짜 정보를 단위가 나노초인 UNIX Timestamp 로 변환하는 기능

rdate라는 이름은 Registration Date ( 등록 날짜 ) 을 줄인 단어임

이 rdate 값을 AddressList 컴포넌트에서 Address 컴포넌트를 생성할 때 key 값으로 사용하면 [ 삭제 ] 가 정상적으로 동작함

 

<< AddressList 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지

(1). Address 컴포넌트를 생성할 때 rdate를 key로 사용하면 각 Addressd 컴포넌트 마다 고유한 key 값이 될 수 있음

(2). 주소 정보를 수정했을 때도 rdate를 같이 담아 전달해야하므로 Address 컴포넌트로 rdate를 전달함

 

<< Address 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지

(1). Address 컴포넌트가 생성될 때 전달된 rdate 를 저장

  index와 마찬가지로 rdate는 변경하지 않을것이기 때문에 setter는 버림

(2). 주소 정보를 [ 수정 ] 할 때 rdate가 빠지면 안되므로 rdate까지 함께 담도록 수정


 

이제 주소록 삭제 기능이 제대로 동작하는지 확인해보자

더보기

주소록 삭제 기능을 처음 눌렀을 때는 제대로 동작하는데 두 번째 부터는 아까와 같이 엉뚱한 주소 정보가 삭제됨

왜그럴까?

"map 함수를 사용해 컴포넌트를 출력할 때 컴포넌트에 반드시 key를 전달해야하며 key는 컴포넌트 간에 구분할 수 있는 유일한 값이어야함"

이 말을 정확히 이해했고 이 말을 했을 때 그렸던 컴포넌트 트리를 현재 상황에 맞게 그려보면 어렵지 않게 알 수 있고 해결 할 수 있는 방법도 찾을 수 있음

 
rdate를 추가하기 전처럼 5개의 주소 정보를 [ 추가 ] 한 상황에서 컴포넌트 트리를 그려보자

 

다음과 같이 그려질 것

여기서 rdate는 16 으로 시작하는 13자리 숫자이지만 임의로 10 ~ 50으로 설정했음

그에 따라 각 Address 컴포넌트의 key도 10 ~ 50을 갖게 됨

 

여기까진 우리가 알고 있는 당연한 상황임

여기서 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황이라고 해보자

그러면 address가 서울3인 Address 컴포넌트에서 AddressList 부모 컴포넌트로 삭제 시그널을 보내 useEffect 훅이 splice 함수를 사용해 addressList의 2번 인덱스(3번째) 주소를 삭제함

이때의 addressList의 상황을 보면 아래와 같음

 

그러니까 아까와 key만 달라졌을뿐 동일한 상황임

이 문제를 해결하려면 어떻게 해야할까?

 

<< AddressList 컴포넌트 >>

변경 사항이 크지 않기 때문에 변경된 부분만 캡쳐한 이미지

key 생성 방식을 변경해 날짜와 index를 함께 조합해서 사용하면 됨

key 생성 방식을 변경하면 Addressd 컴포넌트들은 아래와 같이 만들어짐

 

이때 다시 address가 서울3인 3번째 주소 정보의 [ 삭제 ] 버튼을 누른 상황에서 컴포넌트 트리를 그려 보면 다음과 같음

삭제한 서울3 다음에 있는 서울4, 서울5 Address 컴포넌트의 key가 바뀌었으므로 두 컴포넌트를 새로 그려 화면에 정상적으로 주소 정보가 삭제된걸 볼 수 있음


 

여기까지 길었던 주소록 프로젝트 끝~!

주소록 프로젝트를 통해서 배웠던 부분을 실제로 어떻게 적용하고 그랬을 때 발생하는 문제와 해결하는 과정을 익혔음

이 과정에서 새로운 것들도 배웠으니 잘 정리해둘 것

 

주소록 프로젝트에는 분명히 개선점이 있음

리엑트를 더 이해하고 싶다면 지금까지 배웠던 부분을 다시 한번 복습하고 주소록 프로젝트를 봐보자

개선점을 찾고 개선하는 과정에서 리엑트 뿐만 아니라 개발 실력이 많이 향상될 것

728x90
LIST

<< 학습 목표 >>

1. 컴포넌트 간 데이터를 주고 받기 위해 useContext 훅을 사용할 수 있다.


전 글 ( https://codingaja.tistory.com/84 ) 에서 컴포넌트간에 데이터를 주고 받기 위해 사용한 방식을 useContext 훅을 사용한 방식으로 바꿔보자

글의 제목은 개선이라고 썼지만 프로젝트의 컴포넌트 트리가 간단하므로 개선이라기 보단 바꾼다 가 더 적합함

 

우선 코드를 모두 입력하고 설명을 보자

<< AddressMng 컴포넌트 >>

더보기

import React, {useState} from 'react';
import AddressAdd from './AddressAdd';
import AddressList from './AddressList';

export const AddressContext = React.createContext();

function AddressMng() {
    const [address, setAddress] = useState();

    return(
        <div className="container">
            <div className="text-center">
                <h1>[ 주소록 관리 프로젝트 (useContext) ]</h1>
                <hr />
            </div>

            <AddressContext.Provider value={ {address, setAddress} }>
                <AddressAdd />

                <AddressList/>
            </AddressContext.Provider>
        </div>
    );
}

export default AddressMng;

<< AddressAdd 컴포넌트 >>

더보기

import React, {useRef, useContext} from 'react';
import { AddressContext } from './AddressMng';

function AddressAdd() {
    let nameRef = useRef(null);
    let telRef = useRef(null);
    let addressRef = useRef(null);
    const setAddress = useContext(AddressContext).setAddress;

    let clickAddBtn = () => {
        let address = {
            "name": nameRef.current.value,
            "tel": telRef.current.value,
            "address": addressRef.current.value
        }

        setAddress(address);
    }

    return(
        <div id="add_panel">
            <div className="row align-items-center justify-content-center">
                <div className="col-2">
                    <input type="text" className="form-control" placeholder="이름" ref={nameRef} />
                </div>
                <div className="col-2">
                    <input type="tel" className="form-control" placeholder="연락처" ref={telRef} />
                </div>
                <div className="col-5">
                    <input type="text" className="form-control" placeholder="주소" ref={addressRef} />
                </div>
                <div className="col-2 text-center">
                    <button type="button" className="btn btn-primary" onClick={clickAddBtn}>추가</button>
                </div>
            </div>
            <hr />
        </div>
    );
}

export default AddressAdd;

<< AddressList 컴포넌트 >>

더보기

import React, {useState, useEffect, useContext} from 'react';
import Address from './Address';
import { AddressContext } from './AddressMng';

function AddressList() {
    const [addressList, setAddressList] = useState([]);
    const address = useContext(AddressContext).address;

    useEffect(() => {
        if(address != null) {
            let newAddressList = Array.from(addressList);
            newAddressList.push(address);
    
            setAddressList(newAddressList);
        }
    }, [address]);

    return(
        <div id="list_panel">
            <table className="table text-center">
                <thead>
                    <tr className="row">
                        <th className="col-1" scope="col"></th>
                        <th className="col-2" scope="col">이름</th>
                        <th className="col-2" scope="col">연락처</th>
                        <th className="col-5" scope="col">주소</th>
                        <th className="col-2" scope="col"></th>
                    </tr>
                </thead>
                <tbody>
                    {
                    addressList.map((address, index) => {
                        return <Address key={index} number={address.number} name={address.name} tel={address.tel} address={address.address} />
                    })
                    }
                </tbody>
            </table>
        </div>
    );
}

export default AddressList

 

<< AddressMng 컴포넌트와 AddressAdd 컴포넌트 설명 >>

더보기

 

(1). 부모 컴포넌트와 자식 컴포넌트들이 공유해서 사용할 Context(공용 변수) 생성

(2). 자식 컴포넌트에서 전달할 값을 받기 위한 변수 선언

(3). 자식 컴포넌트들이 Context를 사용할 수 있도록 설정

(4). 전 글 ( https://codingaja.tistory.com/85 ) 에서는 자식 컴포넌트로 간단하게 "Hi" 문자열을 전달했지만 이번에는 address와 setAddress 를 전달

  address는 주소 정보를 저장할 값이지만 setAddress는 setter 함수라는 점에 유의하자

  JS는 함수가 1급 객체이기 때문에 이와 같이 함수를 전달할 수도 있음

 

  자식 컴포넌트로 값들을 보내기 위해 { } 로 묶었다는 점에도 유의하자

   address는 AddressList 컴포넌트에서 사용하고 setAddress는 AddressAdd 컴포넌트에서 사용함

 

(5). AddressMng 부모 컴포넌트가 전달한 setter 를 꺼내기 위해 공용 변수를 import함

(6). 공용 변수에 들어있는 setter를 꺼내기 위해 우선 useContext 훅을 사용함

  conset setAddress = useContext(AddressContext); 만 하게 되면 setAddress 변수에 address 값을 갖고 있는 변수와 setter 함수가 모두 저장됨, 이와 같이 했다면 setAddress.setAddress(address); 로 해야하기에 좀 더 편하게 사용하기 위해 공용 변수가 갖고 있는 setter 함수만 setAddressd 변수에 저장함

(7). 부모 컴포넌트가 공용 변수에 담아서 보내준 setter 를 사용해서 주소 정보 저장


 

<< AddressList 컴포넌트 설명 >>

더보기

 

(1). AddressMng 부모 컴포넌트가 전달한 공용 변수를 사용하기 위해 import함

(2). 공용 변수 내 address 변수의 값(주소 정보)만 꺼냄

(3). 꺼낸 주소 정보를 주소록에 저장


 

바뀐 부분이 크게 없다고 느끼거나 "왜 굳이 useContext 를 사용하지?" 라고 느낄 수 있음

이 글의 처음에 언급했듯 우리 프로젝트의 규모가 작아서 그런것이므로 여기서 알려준 방법대로 부모 컴포넌트와 자식 컴포넌트가 값을 주고 받는 방법을 잘 기억해두면 포트폴리오 또는 현업에서 도움이 많이 될 것

728x90
LIST