지난번에는 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 (
onClick(value)}
style={{
cursor: "pointer",
padding: "5px 10px",
margin: "2px",
}}
>
{value}
);
};
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 (
<>
{ValueList.map((value, index) => (
))}
>
);
};
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 (
onClick(value)}
style={{
cursor: "pointer",
padding: "5px 10px",
margin: "2px",
}}
>
{value}
);
};
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 (
<>
{ValueList.map((value, index) => (
))}
>
);
};
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 (
onClick(value)}
style={{
cursor: "pointer",
padding: "5px 10px",
margin: "2px",
}}
>
{value}
);
};
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 (
<>
{ValueList.map((value, index) => (
))}
Variable
W Statistic
P-value
{Object.entries(results).map(([variableName, stats], index) => (
{variableName}
{stats.W && stats.W.length > 0
? stats.W[0].toFixed(3)
: "N/A"}
{stats.P_Value && stats.P_Value.length > 0
? stats.P_Value[0].toFixed(3)
: "N/A"}
))}
>
);
};
export default Script;
위 동작 방식을 파악 하시고 응용하시면
원하는 데이터를 받아, 내가 원하는 스크립트로 통계를 시행하고, 그 결과를 프론트엔드에 나타내는 것을 어렵지 않게 할 수 있을 겁니다.