EC2 에서 R을 이용한 통계 페이지 만들기

지난번에는 EC2에서 R을 세팅하고 활용하는 방법을 확인했습니다.

이번에는 실제로 어떤 식으로 이것을 활용하면 좋을지 자세히 알아보겠습니다.

우선 목적은 다음과 같습니다.

1. 내 DB에 저장된 데이터 중 특정한 변수에 해당하는 데이터를 선택한다.

2. 이 데이터를 통해서 R 스크립트를 이용하여 데이터의 정규성 검정을 시행한다.

3. 정규성 검정을 시행한 결과를 프론트엔드에서 전달 받아 표시합니다. 

프론트엔드 구성

				
					import React, { useState, useRef } from "react";

const Script = () => {
  const optionListRef = useRef(null);

  const [ValueList, setValueList] = useState([
    "id",
    "name",
    "sex",
    "height",
    "weight",
  ]);

  // 해당 컴포넌트에서 선택된 특정한 스크립트의 타입을 여기 넣습니다.
  const [selectedValues, setSelectedValues] = useState([]);
  const [analysisVariables, setAnalysisVariables] = useState("");

  // 각 옵션을 렌더링하는 데 사용되는 컴포넌트
  const [isFocused, setIsFocused] = useState(false);

  const Option = ({ value, isSelected, onClick }) => {
    return (
      <div
        className={`option ${isSelected ? "select" : ""}`}
        onClick={() => onClick(value)}
        style={{
          cursor: "pointer",
          padding: "5px 10px",

          margin: "2px",
        }}
      >
        {value}
      </div>
    );
  };

  const handleFocus = () => {
    setIsFocused(true);
    optionListRef.current.style.height = "500px"; // 포커스 상태에서의 높이
  };

  const handleBlur = () => {
    setIsFocused(false);
    optionListRef.current.style.height = "100px"; // 블러 상태에서의 기본 높이
  };

  // 옵션 선택/해제 처리 함수
  const handleOptionClick = (value) => {
    const currentIndex = selectedValues.indexOf(value);
    const newSelectedValues = [...selectedValues];

    if (currentIndex === -1) {
      newSelectedValues.push(value); // 선택되지 않은 옵션이면 추가
    } else {
      newSelectedValues.splice(currentIndex, 1); // 이미 선택된 옵션이면 제거
    }

    setSelectedValues(newSelectedValues);
    setAnalysisVariables(newSelectedValues.join(",")); // 선택된 값들을 콤마로 구분된 문자열로 업데이트
  };

  return (
    <>
      <div
        className="monitor"
        style={{ display: "flex", flexDirection: "column" }}
      >
        <div className="three-value-area">
          <div
            className="input-text-3"
            ref={optionListRef}
            tabIndex={0}
            onFocus={handleFocus}
            onBlur={handleBlur}
          >
            {ValueList.map((value, index) => (
              <Option
                key={index}
                value={value}
                isSelected={selectedValues.includes(value)}
                onClick={handleOptionClick}
              />
            ))}
          </div>
        </div>
      </div>
    </>
  );
};

export default Script;

				
			

우선은 이런 식으로 내가 ValueList 에 설정한 값을 사용자가 선택 가능하도록 세팅을 했습니다.

내가 특정 DB로 데이터를 전송하고 있다면, 그 DB의 특정 열들을 미리 설정해두고 그 열들에 들어간 데이터를 정규성 검사하는 것이 목적이니까요.

선택을 한 값은 analysisVariables라는 변수에 담기게 됩니다. 

height 과 weight 을 선택했다면 “height, weight” 이런 식으로 , 으로 구분해서 담기도록 했습니다.

이제 이 정보를 백엔드에 보내서 결과를 가져와보기 위해 코드를 추가합시다. 

				
					import React, { useState, useRef } from "react";

const Script = () => {
  const optionListRef = useRef(null);

  const [ValueList, setValueList] = useState([
    "id",
    "name",
    "sex",
    "height",
    "weight",
  ]);

  // 해당 컴포넌트에서 선택된 특정한 스크립트의 타입을 여기 넣습니다.
  const [selectedValues, setSelectedValues] = useState([]);
  const [analysisVariables, setAnalysisVariables] = useState("");

  // 각 옵션을 렌더링하는 데 사용되는 컴포넌트
  const [isFocused, setIsFocused] = useState(false);

  const Option = ({ value, isSelected, onClick }) => {
    return (
      <div
        className={`option ${isSelected ? "select" : ""}`}
        onClick={() => onClick(value)}
        style={{
          cursor: "pointer",
          padding: "5px 10px",

          margin: "2px",
        }}
      >
        {value}
      </div>
    );
  };

  const handleFocus = () => {
    setIsFocused(true);
    optionListRef.current.style.height = "500px"; // 포커스 상태에서의 높이
  };

  const handleBlur = () => {
    setIsFocused(false);
    optionListRef.current.style.height = "100px"; // 블러 상태에서의 기본 높이
  };

  // 옵션 선택/해제 처리 함수
  const handleOptionClick = (value) => {
    const currentIndex = selectedValues.indexOf(value);
    const newSelectedValues = [...selectedValues];

    if (currentIndex === -1) {
      newSelectedValues.push(value); // 선택되지 않은 옵션이면 추가
    } else {
      newSelectedValues.splice(currentIndex, 1); // 이미 선택된 옵션이면 제거
    }

    setSelectedValues(newSelectedValues);
    setAnalysisVariables(newSelectedValues.join(",")); // 선택된 값들을 콤마로 구분된 문자열로 업데이트
  };

  //코드 추가
  const tableName = "character_info"; // 내 DB 테이블 이름을 지정

  const [results, setResults] = useState({}); // DB에서 가져온 결과를 저장할 공간

  const runAnalysis = async () => {
    // 입력 검증
    if (!analysisVariables) {
      alert("분석을 위한 변수가 입력되지 않았습니다!");
      return;
    }

    try {
      const response = await fetch("your-api", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          table_name: tableName,
          analysisVariables: analysisVariables,
        }),
      });

      if (!response.ok) {
        // 오류가 있는 경우 사용자에게 알립니다.
        alert(`연속형 변수만을 선택하세요.: ${response.status}`);
        return;
      }

      const data = await response.json();
      setResults(data); // 결과 상태 업데이트
    } catch (error) {
      alert(`연속형 변수만을 선택하세요.: ${error.message}`);
    }
  };

  return (
    <>
      <div
        className="monitor"
        style={{ display: "flex", flexDirection: "column" }}
      >
        <div className="three-value-area">
          <div
            className="input-text-3"
            ref={optionListRef}
            tabIndex={0}
            onFocus={handleFocus}
            onBlur={handleBlur}
          >
            {ValueList.map((value, index) => (
              <Option
                key={index}
                value={value}
                isSelected={selectedValues.includes(value)}
                onClick={handleOptionClick}
              />
            ))}
          </div>

          <button className="run-btn" onClick={runAnalysis}>
            Run Script
          </button>
        </div>
      </div>
    </>
  );
};

export default Script;

				
			

63번째 줄부터 코드를 추가했습니다.

특정한 api 주소를 설정하고 그곳으로 필요한 정보를 보내고 있습니다.

결과적으로 반환된 결과를 가져와서 setResults 를 통해 객체 형태로 저장할 계획입니다. 

현재는 요런 느낌이고요.

여기서 id, name 등을 선택하고 Run Script 를 누르면 runAnalysis 라는 동작이 작동하면서 백엔드 라우터가 발동하게 할 겁니다.

이번에는 백엔드로 넘어갑시다.

백엔드 설정

코드 보기 전에 우선 저는 제 DB 내부에 이런 형태의 테이블을 만들었습니다.

아까 프론트엔드에서 선택 가능한 옵션들과 정확히 Column 이름이 일치하죠?

이제 이쪽 DB에 접근할 예정인데 혹시 백엔드에서 DB 접근하는 방식을 모른다면 다음 포스팅을 참고해주세요.

위 포스팅으로 자세한 설명은 생락하겠습니다. 

우선 백엔드 동작을 위해 필요한 모듈을 다운로드 합시다.

				
					npm install express mysql csv-stringify uuid

				
			

저는 이렇게 사용할 예정입니다.

이어서 라우터와 라우터 내부에서 사용할 함수입니다.

				
					app.post("/api/", (req, res) => {
  const { table_name, analysisVariables } = req.body;

  // 분석할 변수를 배열로 변환
  const analysisVariableList = analysisVariables
    .split(",")
    .map((varName) => `\`${varName.trim()}\``)
    .join(", ");

  // SQL 쿼리 작성
  let selectQuery = `SELECT ${analysisVariableList} FROM \`${table_name}\``;

  connection.query(selectQuery, (error, results) => {
    if (error) {
      console.error("Database query error:", error);
      return res.status(500).send("Database query error.");
    }

    // 파일 이름 생성 (table_name + UUID)
    const filename = `/my-path/${table_name}_${uuidv4()}.csv`;

    // 결과 데이터를 CSV로 변환 및 파일 저장
    try {
      const csv = parse(results);
      fs.writeFileSync(filename, csv);
    } catch (err) {
      console.error("Error parsing JSON to CSV:", err);
      return res.status(500).send("Error processing data");
    }

    // R 스크립트 실행
    runNormality(res, filename);
  });
});

function runNormality(res, filename) {
  const { spawn } = require("child_process");
  const scriptPath = "/my-patht/normality_test.R";
  const rProcess = spawn("Rscript", [scriptPath, filename]);
  let output = "";

  rProcess.stdout.on("data", (data) => {
    output += data.toString(); // 데이터 수집
  });

  rProcess.stderr.on("data", (data) => {
    console.error(`R Error: ${data}`);
  });

  rProcess.on("close", (code) => {
    console.log(`R process exited with code ${code}`);

    // 파일 삭제
    fs.unlink(filename, (err) => {
      if (err) {
        console.error("Failed to delete file:", err);
        // 선택적으로 여기에서도 에러 응답을 처리할 수 있음
      }
    });

    if (code !== 0) {
      console.error("R script execution failed with code:", code);
      return res.status(500).send("R script execution failed");
    }
    try {
      const results = JSON.parse(output);
      res.json(results);
    } catch (err) {
      console.error("Failed to parse R output as JSON:", err);
      res.status(500).send("Failed to parse R output as JSON");
    }
  });
}

				
			

코드를 조금씩 뜯어서 함께 보겠습니다. 

				
					  const { table_name, analysisVariables } = req.body;

  // 분석할 변수를 배열로 변환
  const analysisVariableList = analysisVariables
    .split(",")
    .map((varName) => `\`${varName.trim()}\``)
    .join(", ");
				
			

여기서는 전달 받은 데이터를 가지고 올바른 형태로 변환하는 역할을 합니다.

“A, B, C” 이런 문자열 형태로 전달을 받은 analysisVariables 를 [A, B, C] 와 같이 배열 형태로 변경하는 겁니다. 

				
					  // SQL 쿼리 작성
  let selectQuery = `SELECT ${analysisVariableList} FROM \`${table_name}\``;

  connection.query(selectQuery, (error, results) => {
    if (error) {
      console.error("Database query error:", error);
      return res.status(500).send("Database query error.");
    }
				
			

요거는 이제 전달 받은 table_name 에 해당하는 테이블을 찾고, 그 테이블에 배열을 보내서 배열에 해당하는 Column 의 값들을 가져옵니다. 

A, B, C 라는 배열을 받았다면 해당 테이블에서 A, B, C 에 해당하는 열의 값들을 가져오는 거죠. 

만약 없다면 에러 코드를 냅니다. 

				
					    // 파일 이름 생성 (table_name + UUID)
    const filename = `/my-path/${table_name}_${uuidv4()}.csv`;
    // 결과 데이터를 CSV로 변환 및 파일 저장
    try {
      const csv = parse(results);
      fs.writeFileSync(filename, csv);
    } catch (err) {
      console.error("Error parsing JSON to CSV:", err);
      return res.status(500).send("Error processing data");
    }

    // R 스크립트 실행
    runNormality(res, filename);
				
			

이제 특정한 경로에 테이블의 복사본 CSV 파일을 생성합니다.

이 CSV 파일은 uuidv4 를 이용해서 고유한 이름을 가지도록 설정합니다.

그리고 이 경로를 filename 으로 하여 runNormality 라는 함수로 전달을 합니다.

이 함수는 경로에 있는 CSV 에 특정한 동작을 시행하여 결과를 반환할 겁니다. 

 

				
					function runNormality(res, filename) {
  const { spawn } = require("child_process");
  const scriptPath = "/my-patht/normality_test.R";
  const rProcess = spawn("Rscript", [scriptPath, filename]);
  let output = "";

  rProcess.stdout.on("data", (data) => {
    output += data.toString(); // 데이터 수집
  });

  rProcess.stderr.on("data", (data) => {
    console.error(`R Error: ${data}`);
  });

  rProcess.on("close", (code) => {
    console.log(`R process exited with code ${code}`);

    // 파일 삭제
    fs.unlink(filename, (err) => {
      if (err) {
        console.error("Failed to delete file:", err);
        // 선택적으로 여기에서도 에러 응답을 처리할 수 있음
      }
    });

    if (code !== 0) {
      console.error("R script execution failed with code:", code);
      return res.status(500).send("R script execution failed");
    }
    try {
      const results = JSON.parse(output);
      res.json(results);
    } catch (err) {
      console.error("Failed to parse R output as JSON:", err);
      res.status(500).send("Failed to parse R output as JSON");
    }
  });
}
				
			

이건 특정한 경로에 있는 스크립트 파일을 실행하고 그 결과를 반환하기 위한 함수입니다.

filename을 변수로 받아 그 경로에 있는 CSV를 가지고 통계를 돌릴 예정인 거죠. 

이것도 분리해서 한 번 봅시다.

				
					const { spawn } = require("child_process");
const scriptPath = "/my-path/normality_test.R";
const rProcess = spawn("Rscript", [scriptPath, filename]);
let output = "";

				
			

child_prosces 모듈의 spwan 함수를 사용하여 R 스크립트를 실행할 겁니다.

이 함수는 비동기적으로 외부 프로그램을 실행하며 Node.js 와는 독립적으로 작동하게 도비니다.

scriptPath 내부에 실행할 정확한 파일의 경로를 담아야 합니다. 

3번째 코드가 실행을 위한 설정입니다. spwan 의 첫 번째 인자는 실행할 명령어가 들어갑니다. 그 뒤에는 사용할 인자를 차례로 담습니다. 

output 내부에는 R 스크립트의 출력을 수집할 계획입니다.

				
					rProcess.stdout.on("data", (data) => {
    output += data.toString(); // 데이터 수집
});


				
			

rProcess.stdout.on(“data”, callback)는 R 스크립트가 표준 출력으로 데이터를 보낼 때마다 호출됩니다.

수신된 데이터(data)는 바이너리 형태이므로 toString() 메서드를 사용하여 문자열로 변환한 뒤 output에 누적합니다.

				
					rProcess.stderr.on("data", (data) => {
    console.error(`R Error: ${data}`);
});



				
			

rProcess.stderr.on(“data”, callback)는 R 스크립트 실행 중 에러 메시지가 있을 경우 호출됩니다.

				
					rProcess.on("close", (code) => {
    console.log(`R process exited with code ${code}`);




				
			

rProcess.on(“close”, callback)는 R 스크립트 실행이 종료될 때 호출됩니다.
code는 프로세스의 종료 코드를 나타냅니다. 0은 성공을 의미하고, 그 외의 값은 에러를 의미합니다.

				
					fs.unlink(filename, (err) => {
    if (err) {
        console.error("Failed to delete file:", err);
    }
});





				
			

fs.unlink 함수를 사용하여 생성된 CSV 파일을 삭제합니다. 파일 삭제에 실패하면 에러를 콘솔에 기록합니다.

				
					if (code !== 0) {
    console.error("R script execution failed with code:", code);
    return res.status(500).send("R script execution failed");
}
try {
    const results = JSON.parse(output);
    res.json(results);
} catch (err) {
    console.error("Failed to parse R output as JSON:", err);
    res.status(500).send("Failed to parse R output as JSON");
}






				
			

종료 코드가 0이 아니면, 클라이언트에 500 에러와 함께 실패 메시지를 보냅니다.
R 스크립트의 출력(output)을 JSON으로 파싱하여 클라이언트에 응답으로 보냅니다. JSON 파싱에 실패할 경우, 에러 메시지와 함께 500 에러를 반환합니다.

				
					library(jsonlite)  # JSON 처리를 위해 jsonlite 사용

args <- commandArgs(trailingOnly = TRUE)
data <- read.csv(args[1])  # CSV 파일 로드

# 정규성 검사 결과를 저장할 리스트
results <- list()

# 모든 변수에 대해 정규성 검사 수행
for (var in names(data)) {
  # 결측값 제거
  clean_data <- na.omit(data[[var]])

  # 결측값 제거 후 데이터가 충분한지 확인
  if (length(clean_data) > 3) {  # Shapiro-Wilk 테스트는 최소 3개의 데이터 포인트를 요구
    # Shapiro-Wilk 정규성 검사
    shapiro_test <- shapiro.test(clean_data)
    
    # 결과 요약
    results[[var]] <- list(
      W = shapiro_test$statistic,
      P_Value = shapiro_test$p.value
    )
  } else {
    results[[var]] <- list(
      W = NA,
      P_Value = NA,
      Note = "Insufficient data after removing NA values"
    )
  }
}

# 결과를 JSON으로 변환하여 출력
cat(toJSON(results, pretty = TRUE))

				
			

아까 특정 경로에 넣어둔 script 파일이 있죠?

저는 이런 R script 파일을 넣었습니다.

종합

이렇게 실제로 스크립트를 실행하며 받아올 데이터를 테이블로 표시하기 위해 talbe 부분을 프론트엔드에 추가했습니다.

이제 실행 결과를 한 번 보겠습니다. 

name 과 sex 를 선택하고 실행하려 하면 연속형 변수만 선택하라는 메시지가 나타납니다.

 

원하는 변수를 선택하고 실행하면 해당 변수의 W statistic 과 P-value 를 나타내게 됩니다. 

 

마치며...

				
					import React, { useState, useRef } from "react";

const Script = () => {
  const optionListRef = useRef(null);

  const [ValueList, setValueList] = useState([
    "id",
    "name",
    "sex",
    "height",
    "weight",
  ]);

  // 해당 컴포넌트에서 선택된 특정한 스크립트의 타입을 여기 넣습니다.
  const [selectedValues, setSelectedValues] = useState([]);
  const [analysisVariables, setAnalysisVariables] = useState("");

  // 각 옵션을 렌더링하는 데 사용되는 컴포넌트
  const [isFocused, setIsFocused] = useState(false);

  const Option = ({ value, isSelected, onClick }) => {
    return (
      <div
        className={`option ${isSelected ? "select" : ""}`}
        onClick={() => onClick(value)}
        style={{
          cursor: "pointer",
          padding: "5px 10px",

          margin: "2px",
        }}
      >
        {value}
      </div>
    );
  };

  const handleFocus = () => {
    setIsFocused(true);
    optionListRef.current.style.height = "500px"; // 포커스 상태에서의 높이
  };

  const handleBlur = () => {
    setIsFocused(false);
    optionListRef.current.style.height = "100px"; // 블러 상태에서의 기본 높이
  };

  // 옵션 선택/해제 처리 함수
  const handleOptionClick = (value) => {
    const currentIndex = selectedValues.indexOf(value);
    const newSelectedValues = [...selectedValues];

    if (currentIndex === -1) {
      newSelectedValues.push(value); // 선택되지 않은 옵션이면 추가
    } else {
      newSelectedValues.splice(currentIndex, 1); // 이미 선택된 옵션이면 제거
    }

    setSelectedValues(newSelectedValues);
    setAnalysisVariables(newSelectedValues.join(",")); // 선택된 값들을 콤마로 구분된 문자열로 업데이트
  };

  //코드 추가
  const tableName = "character_info"; // 내 DB 테이블 이름을 지정

  const [results, setResults] = useState({}); // DB에서 가져온 결과를 저장할 공간

  const runAnalysis = async () => {
    // 입력 검증
    if (!analysisVariables) {
      alert("분석을 위한 변수가 입력되지 않았습니다!");
      return;
    }

    try {
      const response = await fetch("your api", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          table_name: tableName,
          analysisVariables: analysisVariables,
        }),
      });

      if (!response.ok) {
        // 오류가 있는 경우 사용자에게 알립니다.
        alert(`연속형 변수만을 선택하세요.: ${response.status}`);
        return;
      }

      const data = await response.json();
      setResults(data); // 결과 상태 업데이트
    } catch (error) {
      alert(`연속형 변수만을 선택하세요.: ${error.message}`);
    }
  };

  return (
    <>
      <div
        className="monitor"
        style={{ display: "flex", flexDirection: "column" }}
      >
        <div className="three-value-area">
          <div
            className="input-text-3"
            ref={optionListRef}
            tabIndex={0}
            onFocus={handleFocus}
            onBlur={handleBlur}
          >
            {ValueList.map((value, index) => (
              <Option
                key={index}
                value={value}
                isSelected={selectedValues.includes(value)}
                onClick={handleOptionClick}
              />
            ))}
          </div>

          <button className="run-btn" onClick={runAnalysis}>
            Run Script
          </button>

          <table className="statistics-table">
            <thead>
              <tr>
                <th>Variable</th>
                <th>W Statistic</th>
                <th>P-value</th>
              </tr>
            </thead>
            <tbody>
              {Object.entries(results).map(([variableName, stats], index) => (
                <tr key={index}>
                  <td>{variableName}</td>
                  <td>
                    {stats.W && stats.W.length > 0
                      ? stats.W[0].toFixed(3)
                      : "N/A"}
                  </td>
                  <td>
                    {stats.P_Value && stats.P_Value.length > 0
                      ? stats.P_Value[0].toFixed(3)
                      : "N/A"}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </>
  );
};

export default Script;

				
			

위 동작 방식을 파악 하시고 응용하시면 

원하는 데이터를 받아, 내가 원하는 스크립트로 통계를 시행하고, 그 결과를 프론트엔드에 나타내는 것을 어렵지 않게 할 수 있을 겁니다.