Dockerfile

도커이미지를 작성할 때 어떤 스크립트를 실행시키는 것을 dockerfile 이라고 한다.

 

Dockerfile 작성하기

vi에디터로 아래와 같이 작성 (파일이름을 Dockerfile로 저장해야한다.)

FROM httpd
COPY ./webapp /usr/local/apache2/htdocs
CMD ["httpd-foreground"]

FROM : 기본이 될 이미지를 지정해준다.

COPY : 복사명령어 (./webapp 디렉토리내용을 /usr/local/apache2/htdocs 에 복사하라는 의미 )

CMD : 실행할 명령어 (httpd를 실행시킨다.) → Dockerfile에서 한번만 작성할 수 있다. (여러 개의 CMD를 입력하면 맨 마지막 CMD만 유효하다.)

 

※ COPY와 -v (volume)의 차이

  • COPY는 Dockerfile에서 사용 / -v는 run 옵션 중 하나이다.
  • COPY는 복사이고 -v는 링크를 거는 것이기 때문에 COPY된 파일을 수정해도 원본은 수정되지 않지만 -v 로 지정된 파일을 컨테이너에서 수정하면 원본인 HOST에 있는 파일이 수정된다.

Dockerfile 빌드하기

docker build -t [생성할이미지명]:[태그명] [dockerfile위치]
docker build -t webserver ./

도커파일을 빌드하는 것은 이전 시간에 한 docker commit과 비슷한데 docker build의 경우 Dockerfile을 가지고 이미지를 만드는 차이가 있다고함.

 

두 번째 파라미터로 경로만 넘겨준다면 "Dockerfile" 이름을 가진 파일을 알아서 찾아서 빌드해준다고 함. 

(만약 Dockerfile이 Dockerfile 이름으로 저장되어 있지 않다면 -f 옵션으로 도커파일명 을 명시해주어야한다.)

docker build -t sample -f Dockerfile-base ./

 

Dockerfile 빌드한 이미지 확인하기

docker images

빌드결과가 이미지로 제대로 반영되었는지 확인한다.

useReducer 복습

  • Redux에서 차용한 것
  • state를 하나로 묶어주는 역할 state를 바꿀 때는 action을 dispatch해서 reducer에서 바꾼다.
  • Redux와의 차이점 : Redux는 state를 동기적으로 바꾸는데 반해, reducer는 비동기적으로 바꾼다.

Context API

  • props를 넘겨줄 때 부모와 자식 관계가 깊어질 수록 전달하는 것이 힘들어짐.
  • 그것을 해결하기 위한 API

state를 바꾸고자 할 때 props로 받은 dipatch로 부모 컴포넌트에 액션을 보내는 방식 대신 Context API로 바로 보내는 방식을 사용할 것.

 

MineSearch.jsx

import React from 'react';
import MineTable from "./MineTable";
import {useReducer} from "react/index";
import Form from "./Form";

const initalState = {
    tableData: [],
    timer: 0,
    result: '',
}

const reducer = (state, action)=>{
    switch (action.type){
        default:
            return state;
    }
}
const MineSearch = () => {
    const[state,dispatch]=useReducer(reducer(), initalState);

    return (
        <>
            <Form />
            <div>{state.timer}</div>
            <MineTable />
            <div>{state.result}</div>
        </>
    );
};

export default MineSearch;

 

Form.jsx

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

const Form = () => {
    const [cell,setCell]=useState(10);
    const [row,setRow]=useState(10);
    const [mine,setMine]=useState(20);

    const onChangeRow = useCallback(e=>{
        setRow(e.target.value);
    },[]);

    const onChangeCell = useCallback(e=>{
        setCell(e.target.value);
    },[]);

    const onChangeMine = useCallback((param) => (e)=>{
        console.log(param);
        setMine(e.target.value);
    },[]);

    const onClickButton = useCallback(()=>{

    },[]);

    return (
        <div>
            <input type="number" placeholder="세로" value={row} onChange={onChangeRow}/>
            <input type="number" placeholder="가로" value={cell} onChange={onChangeCell}/>
            <input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine(1)}/>
            <button onClick={onClickButton}>시작</button>
        </div>
    );
};

export default Form;

 

MineTable.jsx

import React from 'react';

const MineTable = () => {
    return (
        <table>
            
        </table>
    );
};

export default MineTable;

 

MineTr.jsx

import React from 'react';

const MineTr = () => {
    return (
        <div>
            
        </div>
    );
};

export default MineTr;

 

MineTd.jsx

import React from 'react';

const MineTd = () => {
    return (
        <div>
            
        </div>
    );
};

export default MineTd;

1. Docker hub 저장소 만들기

docker hub 회원이 없다면 회원가입 먼저 진행!

docker hub에 접속하고 나서 repository를 만들어주자.

저장소명 : vim-ubuntu

 

2.Docker Hub Login

실습하는 docker command창에서 도커허브 로그인을 해야 도커허브에 push할 때 엑세스 거부 에러가 뜨지 않는다.

"denied: requested access to the resource is denied"

docker login

위의 명령어를 입력하면 ID/PW를 입력하라고 나오는데 docker hub의 ID/PW를 입력하면 된다.

 

3. 우분투 이미지 실행

우리가 만들 image의 기초가 될 우분투 이미지를 실행해보자.

docker run -dit --name=myUbuntu ubuntu 
docker attach myUbuntu

4. 우분투 컨테이너에 vim 설치하기

아무것도 하지 않은 기본적인 우분투 컨테이너에는 vi 명령어가 존재하지 않는다고 나온다.

심지어 apt-get install도 안된다. (apt update로 레포지토리 정리를 업데이트해줘야함.)

apt update
apt-get install -y vim

 

vi 에디터 설치가 완료되면 /home/ubuntu 디렉토리로 이동해서 아래와 같이 작성하자.

vi hello

wq! 로 저장하고 나오자.

5. 이미지 커밋

docker commit myUbuntu ilikecoding/vim-ubuntu:1.0
docker images

docker images를 해서 커밋한 이미지가 제대로 저장되었는지 확인한다.

6. 이미지 푸시

docker push ilikecoding/vim-ubuntu:1.0

 

6번 까지 진행이 됐다면 docker hub에 접속해서 이미지가 제대로 푸시 되었는지 확인하면 끝!

 

# 컨테이너 삭제
docker stop $(docker ps -a -q --filter name=myUbuntu)
docker rm $(docker ps -a -q --filter name=myUbuntu)

#이미지 삭제
docker images #이미지 확인
docker rmi [우리가 커밋했던 이미지 ID]
docker rmi [기초가된 ubuntu 이미지 ID]

#이미지 다운로드
docker run -dit ilikecoding/vim-ubuntu:1.0 bash

 

이미지 푸시를 위해 생성되었던 컨테이너, 이미지들을 모두 지우고 docker hub에서 다시 다운로드 받아서 확인해보면 우리가 /home/ubuntu에 만들었던 hello 파일이 그대로 있는것을 확인할 수 있다.

 

 

[TIP]

Ubuntu Container에서 Bash가 실행중인 상태인데 exit를 하면 컨테이너가 종료되어버린다. 컨테이너가 종료되지 않으려면 Ctrl + P ▶Ctrp + Q 를 하면 그대로 컨테이너가 유지되면서 터미널을 나올 수 있다.

1. useCallback 사용하기

보통 함수를 props로 넘겨줄 때 useCallback을 사용하여 넘겨준다. (굳이 props로 안넘기는 상황에서도 많이 사용하는거 같음) 

근데 useCallback 주의점은 함수 내부에서 '바뀌는' state나 props를 사용하는 로직이 있다면 의존배열에 꼭 추가시켜주어야한다.

기억력이 너무 강력해서 처음 state값 그대로 가지고 있어버리는 문제를 발생시킬 수 있다.

 

2. 어떤 값 때문에 리렌더링 되는지 찾는 방법

const Td = ({rowIndex, cellIndex, dispatch, cellData}) => {
	const ref = useRef([]);
    useEffect(()=>{
    	console.log(rowIndex === ref.current[0], cellIndex=ref.current[1], dispatch=ref.current[2],cellData=ref.current[3]);
    	ref.current = [rowIndex, cellIndex, dispatch, cellData];
    },[rowIndex, cellIndex, dispatch, cellData]);
}

console.log에서 false가 나오는 값 때문에 리렌더링이 일어나는 것이다.

 

3. React.memo 사용하기

부모가 리렌더링 될 때 자식의 불필요한 리렌더링을 막고 싶다면 React.memo를 사용하자!

(강의에서는 이것으로 해결함!! 내가 클릭한 td만 리렌더링 되도록)

 

4. useMemo로 컴포넌트 자체를 기억할 수도 있다.

최후의 수단으로 활용해보라고 하심.

내부에 들어있는 내용물이 바뀌었을 때만 새로 랜더링하고 그 외에는 랜더링하지 않는 예제.

const Tr = ({rowData, rowIndex, dispatch})=>{
	...
    return (
    	{
        	Array(rowData.length).fill().map((td,i)=>{
            	useMemo(
                	() => <Td ~ ></Td>,
                    [rowData[i]]
                )
            });
        }
    )
}

 

※ react dev tools로 highlight를 사용해 최적화해야할 것들을 고를 때 빨간색만 조심하면 된다고하심. 실제로 빨간색은 서비스할 때 성능상의 문제가 나타난다고 하셨다.

setState는 비동기이다.

dispatch({type:CLICK_CELL});
dispatch({type:CHANGE_TURN});
console.log(state.turn); //바뀐 turn값이 나오지 않는다.

원래 React의 setState와 useReducer는 state가 다 비동기적으로 바뀐다.

(Redux는 동기적으로 바뀜)

그래서 setState나 dispatch를 한 다음에 바로 state를 console.log로 출력해보면 의도한 값이 안나올 수도 있다.

 

※ 따라서 스테이트를 변경한 후 뭔가 처리할 때는 useEffect를 사용해야한다.

(dispatch나 setState를 한 다음 바로 해당 state를 사용하는 로직이 나타나면 의도한 값이 아닐 수 있어서 에러 발생 가능성이 높다.)

 

강의에서는 테이블 클릭 이벤트 ▶ CLICK_CELL 액션 생성 ▶ dispatch 호출 ▶ reducer 호출 ▶tableData, recentCell state변경 ▶ useEffect에서 변경된 tableData를 기반으로 승리자가 있는지 검사 할 때 사용했다.

useEffect(()=>{
	const [row,cell] = recentCell;
    if(row < 0) return ; //최초로 랜더링 될 때 실행되는 것 방지
    //승리자가 있는지 검사
    //무승부 검사
    //턴변경
    ...
},[state.recentCell]);

 

강의에서의 비동기 state변경 문제

강의 내용 중에 dispatch가 연달아 발생하는 곳이 있는데 여기서 문제가 발생한다.

CLICK_CELL을 dispatch 하면 reducer에서는 CLICK_CELL에 해당하는 로직이 수행되는데 그 와중에 바로 CHANGE_TURN까지 같이 되어버려서 CLICK_CELL이 제대로 작동하지 않는 현상이 있었다. (CLICK_CELL로직 중에 state.turn을 사용하는 곳이 있었음. 원래 O가 들어가야하는데 X가 들어가는 문제가 생김. 클릭한 당시의 나의 턴이 아닌 상대방 턴으로 넘어가버림.)

이게 setState는 모아서 처리하는 것 처럼 dispach도 모아서 한번에 처리하면서 그렇게 된건가? 싶기도함..

 

여기서 정말 다시 강조하심..

제일 중요 : 디스패치가 스테이트를 바꾸는게 비동기이다.

 

정리 - 왜 useReducer를 사용하는가?

[state관리의 용이성]

  • 이전에는 state를 여러 개 만들었는데 그 state들이 나중에 10개, 100개가 될 수가 있다.
  • 관리해야하는 변수들이 너무 많아지고 setTable, setTableData, setTurn 이러한 것들이 너무 많아져서 가독성을 헤칠수도 있다.
  • 그래서 한번에 state를 모아서 처리하는 것이 필요하다. (initialState를 객체로 만드는 이유)

[setState 함수 관리의 용이성]

  • state를 한번에 모아서 관리하므로 setState함수들도 한방에 처리해주는 것도 필요함. 그것이 dispatch, action, reducer의 역할이다.

React가 Redux의 개념을 도입해서 useReducer가 생기게 된 것.

 

정리 - useReducer 실행순서

  • 한번에 모아둔 state들은 액션을 통해서만 변경한다.
  1. 이벤트 발생
  2. action 생성 (action type == action name)
  3. action 실행 (dispatch)
  4. reducer가 action을 수행함. == 우리가 정의해둔 대로 state를 바꾼다.
  5. state를 바꿀 때는 불변성을 지키는 것이 매우 중요!

액션 처리 순서

이벤트 발생 ▶ 액션생성 ▶ dispatch 호출 ▶ reducer에서 액션 처리

 

액션 타입들은 export 해주는 게 좋다.

export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';

자식이나 다른 js에서도 dispatch를 호출할 때 액션 타입을 가져다 쓰기위해서 export를 붙인다.

하지만 내가 직접 코딩한 경우 자식에서 dispatch를 호출하는 경우가 없었기 때문에 사실 붙이지 않아도 무방했다.

 

useReducer / redux 에서는 불변성을 꼭 지켜야한다. (사실 react 전체적으로 중요함)

setState함수를 설명할 때도 불변성에 대해서 계속 설명을 했었다.

reducer함수도 결국에는 state를 변경하는 함수이기 때문에 state를 변경할 때 객체상태(object state)는 무조건 불변성을 지켜주어야한다.

...
case CLICK_CELL:
	const tableData = [...state.tableData];
    tableData[row] = [...tableData[row]];
    tableData[row][cell] = state.turn;
	return {
    	...state,
        tableData,
    }

...

나중에는 immer.js라는 라이브러리로 가독성과 편리함을 지킬 수 있다고한다. (무료강좌에서는 immer의 존재만 알고있으라고하심)

 

불변성을 지킨다라는 것은?

메모리영역에서 값을 변경할 수 없게 한다. (값을 바꿀 때 값이 아닌 콜스택의 주소값이 바뀌게 한다.)

그렇다면 왜 react에서 불변성을 지켜야하는가?

state변화 감지 기준이 콜스택의 주소값이기 때문에 콜스택의 주소값을 변경해야 state가 변경되었구나! 하고 리렌더링을 시켜주기 때문이다.

원시값은 그냥 변경해도 불변성이 지켜지지만 (콜스택의 주소값이 변경되지만) 참조타입의 경우 콜스택의 주소값이 변경되지 않기 때문에 spread연산자나 immer 라이브러리를 사용해야 한다.

 

const a = {b:1, c:2};
const b = a;
console.log(b===a); //true : 불변성이 지켜지지 않는다! (객체가 같다는 것은 참조주소가 같다는 것)

const c = {...a};
console.log(c === a); //false : 불변성이 지켜짐.

위의 내용은 spread연션자로 불변성을 지킨 예시이다. 그냥 대입해버리면 주소값이 대입되어버려서 불변성이 지켜지지 않지만 spread 연산자로 얉은 복사를 하면 불변성이 지켜지는 것을 볼 수 있다.

 

ContextAPI를 쓰는 경우

아직 배우지 않았지만 강좌에서 dispatch 함수를 tictactoe ▶ table ▶ tr ▶td 까지 넘겨줘야하는 경우가 있었다. 만약에 부모~넘겨줘야하는 자식 컴포넌트 사이에 10개의 컴포넌트가 있으면 얼마나 힘들까?

이럴 때 쓰는 API가 ContextAPI 라고 한다. tictactoe에서 바로 td로 props를 넘길 수 있게 해준다고 한다.

강좌에서는 ContextAPI의 필요성을 느끼기 위해서 없이 넘기셨다고 함.

 

참고

https://narup.tistory.com/268

 

[React] 불변성이란? 불변성을 지켜야 하는 이유

1. 개요 React를 빠르게 배울 때 값을 변경할 때 useState를 사용해야 한다, 불변성을 유지해야 한다, immer를 사용해야 한다, spread 연산자를 사용해야 한다, 공식처럼 생각하고 있었는데 javascript의 메

narup.tistory.com

 

import React, {useCallback, useEffect, useReducer} from 'react';
import Table from "./Table";

const initialState = {
    winner: '',
    turn : 'O',
    tableData : [['','',''],['','',''],['','','']],
    recentPos: [], //해당 값이 바뀔 때마다 useEffect에서 게임이 끝났는지 체크, 턴 변경
}

const SET_WINNER = "SET_WINNER";
const CLICK_CELL = 'CLICK_CELL';
const TURN_CHANGE = 'TURN_CHANGE';
const RESET_GAME = 'RESET_GAME';

const reducer = (state,action) => {
    switch (action.type){
        case SET_WINNER:
            return {
                ...state,
                winner: action.winner,
            }
        case CLICK_CELL:
            const {row,cell} = action;
            const tableData = [...state.tableData];
            tableData[row] = [...tableData[row]];
            if(tableData[row][cell] === ''){
                tableData[row][cell] = state.turn;
                return {
                    ...state,
                    tableData,
                    recentPos: [row,cell],
                }
            }
            return {
                ...state,
            }
        case TURN_CHANGE:
            return {
                ...state,
                turn : state.turn === 'O' ? 'X' : 'O',
            }
        case RESET_GAME:
            return {
                winner: '',
                turn : 'O',
                tableData : [['','',''],['','',''],['','','']],
                recentPos: [], //해당 값이 바뀔 때마다 useEffect에서 게임이 끝났는지 체크, 턴 변경
            }
    }
}

const TicTacToe = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const tableOnClick = useCallback((e) => {
        if(state.winner !== '') return ;
        const {target} = e;
        const tr = target.parentElement;
        const rowIndex = tr.rowIndex;
        const cellIndex = target.cellIndex;
        dispatch({type:CLICK_CELL, row:rowIndex, cell:cellIndex});
    },[state.winner]);

    const winnerCheck = ({recentPos, tableData, turn}) => {
        let end = true;
        //가로줄 검사

        tableData[recentPos[0]].forEach(value => {
            if(value !== turn) end = false;
        });
        if(end) return true;
        //세로줄 검사
        end=true;
        tableData.forEach((arr,index)=>{
            if(arr[recentPos[1]] !== turn) end = false;
        });
        if(end)return true;

        //대각선 검사
        if((tableData[0][0] === turn && tableData[1][1] === turn && tableData[2][2] === turn)
            || (tableData[0][2] === turn && tableData[1][1] === turn && tableData[2][0] === turn) ){
            end = true;
        }
        return end;
    }
    const fullCheck = (tableData) => {
        let isFull = true;
        tableData.forEach(arr => arr.forEach(v => {
            if(v === '')isFull = false;
        }));
        return isFull;
    }
    const onResetClick = () => {
        dispatch({type:RESET_GAME});
    }

    useEffect(()=>{
        const {recentPos, tableData, turn} = state;
        if(recentPos.length ===0)return ;
        const end = winnerCheck(state);
        if(end){
            //승리자 나옴.
            dispatch({type:SET_WINNER, winner:turn});
        } else {
            if(fullCheck(tableData)){
                dispatch({type:SET_WINNER, winner:'draw!!!'});
            }else{
                dispatch({type:TURN_CHANGE});
            }
        }
    },[state.recentPos]);

    return (
        <>
            <Table onClick={tableOnClick} tableData={state.tableData}/>
            {
                state.winner &&
                <>
                    <div>WINNER IS {state.winner}</div>
                    <button onClick={onResetClick}>RESET</button>
                </>
            }
        </>
    );
};

export default TicTacToe;

볼륨연결

볼륨연결은 폴더를 연결한다고 생각하면 된다.(링크)

호스트의 폴더를 컨테이너의 디렉토리에 마운트해서 호스트에서 해당 폴더에 파일을 생성,삭제,수정을 하면 컨테이너에 바로 적용되도록 할 수 있다.

 

보통 이미지에 들어있는 os들은 최소설치이기 때문에 vi, sudo 같은 명령어가 안되는 경우가 많아서 호스트에서 직접 파일을 수정해서 넣어주면 더 편리한 경우가 많다!

 

 

실습 (목표 : 기본적으로 제공되는 index.html 수정)

1. 아파치 컨테이너 실행 (볼륨 연결X)

docker run -d httpd
docker exec -it [CONTAINER ID] bash

index.html 파일은 /usr/local/apache2/htdocs 에 위치해있다. 해당 파일을 수정하면 웹서버(아파치)에 접근했을 때 나오는 기본 페이지를 변경할 수 있다.

vi나 sudo 같은 명령어가 없다고 나온다.

 

 

2. 볼륨을 연결한 아파치 컨테이너 실행 (볼륨 연결O)

//볼륨에 연결하기 예제
docker run -d -v [HOST_PATH]:[CONTAINER_PATH] [IMAGE]
//볼륨 연결 실습
docker run -d -p 9097:80 -v /home/docker_mount/lecture:/usr/local/apache2/htdocs --name=myHttpd httpd

-v(volume)옵션을 통해 HOST ↔ CONATINER 간 디렉토리를 손쉽게 마운트 할 수 있다.

 

해당 상태에서 웹서버에 접근하면 HOST디렉토리에는 아무것도 없으므로 아래와 같은 화면이 나온다. (기본적으로 생성되는 index.html은  It works! 문구가 표시되도록 되어있다.)

이제 호스트OS에서 설정해둔 디렉토리 (/home/docker_mount/lecture)에 가서 index.html을 생성해보자.

cd [볼륨마운트한 경로]
vi index.html

호스트OS에서 생성한 index.html

 

이렇게 파일을 생성한 후 아파치 서버에 접속해보면 아래와 같이 잘나오는 것을 확인할 수 있다.

 

 

정리 : 호스트OS와 컨테이너간 디렉토리 마운트를 위해서는 볼륨을 이용하자. (docker run -v [HOST]:[CONTAINER])

attach로 접근할 수 없는 경우

OS만 있는 컨테이너와 다르게 다른 command가 있는 컨테이너들은 단순히 attach로 리눅스 쉘에 접근할 수가 없다.

docker run -d -p 8080:80 httpd

이렇게 컨테이너를 실행시킨 후 

docker attach [방금실행시킨 httpd ID]

attach 명령어로 리눅스 쉘에 접근하려고 하면 접근이 안된다.

run할 때 -it를 붙이지도 않았을 뿐더러 실행되고 있는 명령어가 /bin/bash가 아니라 httpd-foreground 이기 때문.

 

그렇다면 실행할 때 -it 옵션을 준다면?

docker run -dit httpd
docker run -dit httpd bash

 

이렇게 실행해서 docker attach를 하면 어떨까? 컨테이너가 바로 종료되어버린다.

두번째 줄에있는 bash명령어를 줘도 마찬가지.

 

다른 명령어가 실행되고 있을 때 터미널에 접근하고 싶다면?

docker exec -it [CONTAINER_ID] bash

exec -it 명령어로 해결 할 수 있다. 상호작용할 수 있는 bash를 실행시켜준다.

옵션인 -it의 의미는 이전 강의에와 마찬가지인것 같다.

-i : interactive 상호작용

-t : terminal 모드 /bin/bash로 상호작용한다.

 

 

 

 


정리
1. os(ubuntu) 이미지일 때 터미널 실행 법
docker run -dit ubuntu
docker attach [CONTAINER_ID]

2. while process (httpd) 이미지 일 때
docker run -d -p 8080:80 httpd (docker run -dit를 해도 상관없음)
docker exec -it [CONTAINER_ID] bash

도커는 이미지 실행 시에 command를 지정하게 되어있는데 아파치같은 것은 아파치 실행 명령어가 command여서 attach로 접근하면 bash 접근불가.
이럴 때는 docker exec -it [CONTAINER_ID] bash 로 상호작용할 수 있도록 해주면 된다.

디스패치 / 액션 / 리듀서

useReducer를 사용하면 state를 변경해야할 때 dispatch함수를 호출하고, 액션 객체를 전달해서 state를 변경한다.

  • dispatch : 액션을 실행해주는 함수.
  • 액션 객체 : dispatch에 전달되는 객체로 리듀서가 실행해야할 로직을 알려주는 action.type과 리듀서가 변경해야할 state값을 가지고 있다.
  • reducer : 액션 객체를 해석해서 state를 직접 바꿔주는 함수. (리듀서는 dispatch가 실행될 때 마다 자동으로 호출된다.) --> reducer함수에서 반환하는 값은 새로운 state이다.

useReducer를 사용하면 state 변경 순서는 아래와 같아진다.

  1. 이벤트 발생
  2. dispatch 호출 (이벤트 객체를 만들어서 dispatch 함수 호출 시 이벤트 객체를 넘겨준다.)
  3. dispatch가 호출되면 자동으로 리듀서가 호출되면서 액션 객체를 해석하여 특정 로직 실행 (state변경)

 

reducer함수에서 주의해야할 점은 불변성을 꼭 지켜야한다는 것!

reducer함수에서 반환하는 값은 새로운 state인데 항상 새로운 객체를 만들어 바뀐 값만 바꿔줘야한다. 이를 위해 spread연산자를 활용해서 불변성을 지키자!

 

ACTION TYPE

  • 액션 타입은 리듀서함수에서 어떤 state를 수정할 로직을 실행해야할지 결정하는 중요한 정보이다.
  • 액션 타입은 상수로 따로 빼두는 것이좋다.
  • 액션 타입은 대문자로 하는 것이 커뮤니티 규칙이다.

 

 

TicTacToe.jsx

import React, {useCallback, useReducer, useState} from "react";
import Table from "./Table";

//state 초기값
const initialState = {
    winner : '',
    turn: 'O',
    tableData:[['','',''],['','',''],['','','']],
}
//액션 타입은 상수로 따로 빠두는것이 좋다.
//액션의 이름은 대문자로 하는것이 커뮤니티 규칙!
const SET_WINNER = 'SET_WINNER';

//리듀서 == 함수
const reducer = (state, action) => {
    //리듀서는 액션을 디스패치 할 때마다 실행된다.
    switch(action.type){
        case SET_WINNER:
            //state.winner = action.winner; 이렇게 하면 안된다!!!!
            //우리는 항상 새로운 객체를 만들어서 바뀐 값만 바꿔줘야한다. 불변성 지켜!.
            return {
                ...state,
                winner: action.winner,
            }

    }
}

const TicTacToe = () => {
    /*
    const [winner, setWinner] = useState('');
    const [turn, setTurn] = useState('O');
    const [tableData, setTableData] = useState([['','',''],['','',''],['','','']]);
    */
    const [state, dispatch] = useReducer(reducer, initialState);

    //컴포넌트에 있는 함수들은 다 useCallback으로 감싸주자.
    const onClickTable = useCallback(()=>{
        //dispatch는 액션을 실행해주는 함수이다. dispatch에 전달되는 값은 액션객체이다.
        //액션 객체를 해석해서 state를 직접 바꿔주는 역할을 하는 것이 리듀서이다.
        dispatch({type:SET_WINNER, winner:'O'});
    },[]);

    return(
        <>
            <Table onClick={onClickTable} tableData={state.tableData}/>
            {
                state.winner && <div>{state.winner}님의 승리</div>
            }
        </>
    );
}

export default TicTacToe;

 

Table.jsx

import React from 'react';
import Tr from "./Tr";

const Table = ({onClick, tableData}) => {
    return (
        <table onClick={onClick}>
            {Array(tableData.length).fill().map((tr,i)=><Tr rowData={tableData[i]}/>)}
        </table>
    );
};

export default Table;

 

Tr.jsx

import React from 'react';
import Td from "./Td";

const Tr = ({rowData}) => {
    return (
        <tr>
            {
                Array(rowData.length).fill().map((td)=><Td/>)
            }
        </tr>
    );
};

export default Tr;

 

+ Recent posts